先睹為快,Go2 Error 的掙扎之路
本文轉(zhuǎn)載自微信公眾號(hào)「腦子進(jìn)煎魚(yú)了」,作者陳煎魚(yú) 。轉(zhuǎn)載本文請(qǐng)聯(lián)系腦子進(jìn)煎魚(yú)了公眾號(hào)。
大家好,我是煎魚(yú)。
自從 Go 語(yǔ)言在國(guó)內(nèi)火熱以來(lái),除去泛型,其次最具槽點(diǎn)的就是 Go 對(duì)錯(cuò)誤的處理方式,一句經(jīng)典的 if err != nil 暗號(hào)就能認(rèn)出你是一個(gè) Go 語(yǔ)言愛(ài)好者。
自然,大家對(duì) Go error 的關(guān)注度更是高漲,Go team 也是,因此在 Go 2 Draft Designs 中正式提到了 error handling(錯(cuò)誤處理)的相關(guān)草案,希望能夠在未來(lái)正式的解決這個(gè)問(wèn)題。
在今天這篇文章中,我們將一同跟蹤 Go2 error,看看他是怎么 “掙扎” 的,能不能破局?
為什么要吐槽 Go1
要吐槽 Go1 error,就得先知道為什么大家到底是在噴 Error 哪里處理的不好。在 Go 語(yǔ)言中,error 其實(shí)本質(zhì)上只是個(gè) Error 的 interface:
- type error interface {
 - Error() string
 - }
 
實(shí)際的應(yīng)用場(chǎng)景如下:
- func main() {
 - x, err := foo()
 - if err != nil {
 - // handle error
 - }
 - }
 
單純的看這個(gè)例子似乎沒(méi)什么問(wèn)題,但工程大了后呢?
顯然 if err != nil 的邏輯是會(huì)堆積在工程代碼中,Go 代碼里的 if err != nil 甚至?xí)_(dá)到工程代碼量的 30% 以上:
- func main() {
 - x, err := foo()
 - if err != nil {
 - // handle error
 - }
 - y, err := foo()
 - if err != nil {
 - // handle error
 - }
 - z, err := foo()
 - if err != nil {
 - // handle error
 - }
 - s, err := foo()
 - if err != nil {
 - // handle error
 - }
 - }
 
暴力的對(duì)比一下,就發(fā)現(xiàn)四行函數(shù)調(diào)用,十二行錯(cuò)誤,還要苦練且精通 IDE 的快速折疊功能,還是比較麻煩的。
另外既然是錯(cuò)誤處理,那肯定不單單是一個(gè) return err 了。在工程實(shí)踐中,項(xiàng)目代碼都是層層嵌套的,如果直接寫(xiě)成:
- if err != nil {
 - return err
 - }
 
在實(shí)際工程中肯定是不行。你怎么知道具體是哪里拋出來(lái)的錯(cuò)誤信息,實(shí)際出錯(cuò)時(shí)只能瞎猜。大家又想出了 PlanB,那就是加各種描述信息:
- if err != nil {
 - logger.Errorf("煎魚(yú)報(bào)錯(cuò) err:%v", err)
 - return err
 - }
 
雖然看上去人模人樣的,在實(shí)際出錯(cuò)時(shí),也會(huì)遇到新的問(wèn)題,因?yàn)槟阋ゲ檫@個(gè)錯(cuò)誤是從哪里拋出來(lái)的,沒(méi)有調(diào)用堆棧,單純幾句錯(cuò)誤描述是難以定位的。
這時(shí)候就會(huì)發(fā)展成到處打錯(cuò)誤日志:
- func main() {
 - err := bar()
 - if err != nil {
 - logger.Errorf("bar err:%v", err)
 - }
 - ...
 - }
 - func bar() error {
 - _, err := foo()
 - if err != nil {
 - logger.Errorf("foo err:%v", err)
 - return err
 - }
 - return nil
 - }
 - func foo() ([]byte, error) {
 - s, err := json.Marshal("hello world.")
 - if err != nil {
 - logger.Errorf("json.Marshal err:%v", err)
 - return nil, err
 - }
 - return s, nil
 - }
 
雖然到處打了日志,就會(huì)變成錯(cuò)誤日志非常多,一旦出問(wèn)題,人肉可能短時(shí)間內(nèi)識(shí)別不出來(lái)。
最常見(jiàn)的就是到 IDE 上 ctrl + f 搜索是在哪出錯(cuò)。同時(shí)在實(shí)際應(yīng)用中我們會(huì)自定義一些錯(cuò)誤類型,在 Go 則需要各種判斷和處理:
- if err := dec.Decode(&val); err != nil {
 - if serr, ok := err.(*json.SyntaxError); ok {
 - ...
 - }
 - return err
 - }
 
你得判斷不等于 nil,還得對(duì)自定義的錯(cuò)誤類型進(jìn)行斷言,整體來(lái)講比較繁瑣。
匯總來(lái)講,Go1 錯(cuò)誤處理的問(wèn)題至少有:
- 在工程實(shí)踐中,if err != nil 寫(xiě)的煩,代碼中一大堆錯(cuò)誤處理的判斷,占了相當(dāng)?shù)谋壤?,不夠?yōu)雅。
 - 在排查問(wèn)題時(shí),Go 的 err 并沒(méi)有其他堆棧信息,只能自己增加描述信息,層層疊加,打一大堆日志,排查很麻煩。
 - 在驗(yàn)證和測(cè)試錯(cuò)誤時(shí),要自定義錯(cuò)誤(各種判斷和斷言)或者被迫用字符串校驗(yàn)。
 
Go1.13 的挽尊
在 2019 年 09 月,Go1.13 正式發(fā)布。其中兩個(gè)比較大的兩個(gè)關(guān)注點(diǎn)分別是包依賴管理 Go modules 的轉(zhuǎn)正,以及錯(cuò)誤處理 errors 標(biāo)準(zhǔn)庫(kù)的改進(jìn):
Error wrapping
在本次改進(jìn)中,errors 標(biāo)準(zhǔn)庫(kù)引入了 Wrapping Error 的概念,并增加了 Is/As/Unwarp 三個(gè)方法,用于對(duì)所返回的錯(cuò)誤進(jìn)行二次處理和識(shí)別。
同時(shí)也是將 Go2 error 預(yù)規(guī)劃中沒(méi)有破壞 Go1 兼容性的相關(guān)功能提前實(shí)現(xiàn)了。
簡(jiǎn)單來(lái)講,Go1.13 后 Go 的 error 就可以嵌套了,并提供了三個(gè)配套的方法。例子:
- func main() {
 - e := errors.New("腦子進(jìn)煎魚(yú)了")
 - w := fmt.Errorf("快抓?。?w", e)
 - fmt.Println(w)
 - fmt.Println(errors.Unwrap(w))
 - }
 
輸出結(jié)果:
- $ go run main.go
 - 快抓?。耗X子進(jìn)煎魚(yú)了
 - 腦子進(jìn)煎魚(yú)了
 
在上述代碼中,變量 w 就是一個(gè)嵌套一層的 error。最外層是 “快抓?。?rdquo;,此處調(diào)用 %w 意味著 Wrapping Error 的嵌套生成。因此最終輸出了 “快抓?。耗X子進(jìn)煎魚(yú)了”。
需要注意的是,Go 并沒(méi)有提供 Warp 方法,而是直接擴(kuò)展了 fmt.Errorf 方法。而下方的輸出由于直接調(diào)用了 errors.Unwarp 方法,因此將 “取” 出一層嵌套,最終直接輸出 “腦子進(jìn)煎魚(yú)了”。
對(duì) Wrapping Error 有了基本理解后,我們簡(jiǎn)單介紹一下三個(gè)配套方法:
- func Is(err, target error) bool
 - func As(err error, target interface{}) bool
 - func Unwrap(err error) error
 
errors.Is
方法簽名:
- func Is(err, target error) bool
 
方法例子:
- func main() {
 - if _, err := os.Open("non-existing"); err != nil {
 - if errors.Is(err, os.ErrNotExist) {
 - fmt.Println("file does not exist")
 - } else {
 - fmt.Println(err)
 - }
 - }
 - }
 
errors.Is 方法的作用是判斷所傳入的 err 和 target 是否同一類型,如果是則返回 true。
errors.As
方法簽名:
- func As(err error, target interface{}) bool
 
方法例子:
- func main() {
 - if _, err := os.Open("non-existing"); err != nil {
 - var pathError *os.PathError
 - if errors.As(err, &pathError) {
 - fmt.Println("Failed at path:", pathError.Path)
 - } else {
 - fmt.Println(err)
 - }
 - }
 - }
 
errors.As 方法的作用是從 err 錯(cuò)誤鏈中識(shí)別和 target 相同的類型,如果可以賦值,則返回 true。
errors.Unwarp
方法簽名:
- func Unwrap(err error) error
 
方法例子:
- func main() {
 - e := errors.New("腦子進(jìn)煎魚(yú)了")
 - w := fmt.Errorf("快抓?。?w", e)
 - fmt.Println(w)
 - fmt.Println(errors.Unwrap(w))
 - }
 
該方法的作用是將嵌套的 error 解析出來(lái),若存在多級(jí)嵌套則需要調(diào)用多次 Unwarp 方法。
民間自救 pkg/errors
Go1 的 error 處理固然存在許多問(wèn)題,因此在 Go1.13 前,早已有 “民間” 發(fā)現(xiàn)沒(méi)有上下文調(diào)試信息在實(shí)際工程應(yīng)用中存在嚴(yán)重的體感問(wèn)題。
因此 github.com/pkg/errors 在 2016 年誕生了,該庫(kù)也已經(jīng)受到了極大的關(guān)注。
官方例子如下:
- type stackTracer interface {
 - StackTrace() errors.StackTrace
 - }
 - err, ok := errors.Cause(fn()).(stackTracer)
 - if !ok {
 - panic("oops, err does not implement stackTracer")
 - }
 - st := err.StackTrace()
 - fmt.Printf("%+v", st[0:2]) // top two frames
 - // Example output:
 - // github.com/pkg/errors_test.fn
 - // /home/dfc/src/github.com/pkg/errors/example_test.go:47
 - // github.com/pkg/errors_test.Example_stackTrace
 - // /home/dfc/src/github.com/pkg/errors/example_test.go:127
 
簡(jiǎn)單來(lái)講,就是對(duì) Go1 error 的上下文處理進(jìn)行了優(yōu)化和處理,例如類型斷言、調(diào)用堆棧等。若有興趣的小伙伴可以自行到 github.com/pkg/errors 進(jìn)行學(xué)習(xí)。
另外你可能會(huì)發(fā)現(xiàn) Go1.13 新增的 Wrapping Error 體系與 pkg/errors 有些相像。
你并沒(méi)有體會(huì)錯(cuò),Go team 接納了相關(guān)的意見(jiàn),對(duì) Go1 進(jìn)行了調(diào)整,但調(diào)用堆棧這塊因綜合原因暫時(shí)沒(méi)有納入。
Go2 error 要解決什么問(wèn)題
在前面我們聊了 Go1 error 的許多問(wèn)題,以及 Go1.13 和 pkg/errors 的自救和融合。你可能會(huì)疑惑,那...Go2 error 還有出場(chǎng)的機(jī)會(huì)嗎?即使 Go1 做了這些事情,Go1 error 還有問(wèn)題嗎?
并沒(méi)有解決,if err != nil 依舊一把梭,目前社區(qū)聲音依然認(rèn)為 Go 語(yǔ)言的錯(cuò)誤處理要改進(jìn)。
Go2 error proposal
在 2018 年 8 月,官方正式公布了 Go 2 Draft Designs,其中包含泛型和錯(cuò)誤處理機(jī)制改進(jìn)的初步草案:
Go2 Draft Designs
注:Go1.13 正式將一些不破壞 Go1 兼容性的 Error 特性加入到了 main branch,也就是前面提到的 Wrapping Error。
錯(cuò)誤處理(Error Handling)
第一個(gè)要解決的問(wèn)題就是大量 if err != nil 的問(wèn)題,針對(duì)此提出了 Go2 error handling 的草案設(shè)計(jì)。
簡(jiǎn)單例子:
- if err != nil {
 - return err
 - }
 
優(yōu)化后的方案如下:
- func CopyFile(src, dst string) error {
 - handle err {
 - return fmt.Errorf("copy %s %s: %v", src, dst, err)
 - }
 - r := check os.Open(src)
 - defer r.Close()
 - w := check os.Create(dst)
 - handle err {
 - w.Close()
 - os.Remove(dst) // (only if a check fails)
 - }
 - check io.Copy(w, r)
 - check w.Close()
 - return nil
 - }
 
主函數(shù):
- func main() {
 - handle err {
 - log.Fatal(err)
 - }
 - hex := check ioutil.ReadAll(os.Stdin)
 - data := check parseHexdump(string(hex))
 - os.Stdout.Write(data)
 - }
 
該提案引入了兩種新的語(yǔ)法形式,首先是 check 關(guān)鍵字,其可以選中一個(gè)表達(dá)式 check f(x, y, z) 或 check err,其將會(huì)標(biāo)識(shí)這是一個(gè)顯式的錯(cuò)誤檢查。
其次引入了 handle 關(guān)鍵字,用于定義錯(cuò)誤處理程序流轉(zhuǎn),逐級(jí)上拋,依此類推,直到處理程序執(zhí)行 return 語(yǔ)句,才正式結(jié)束。
錯(cuò)誤值打印(Error Printing)
第二個(gè)要解決的問(wèn)題是錯(cuò)誤值(Error Values)、錯(cuò)誤檢查(Error Inspection)的問(wèn)題,其引申出錯(cuò)誤值打印(Error Printing)的問(wèn)題,也可以認(rèn)為是錯(cuò)誤格式化的不便利。
官方針對(duì)此提出了提出了 Error Values 和 Error Printing 的草案設(shè)計(jì)。
簡(jiǎn)單例子如下:
- if err != nil {
 - return fmt.Errorf("write users database: %v", err)
 - }
 
優(yōu)化后的方案如下:
- package errors
 - type Wrapper interface {
 - Unwrap() error
 - }
 - func Is(err, target error) bool
 - func As(type E)(err error) (e E, ok bool)
 
該提案增加了錯(cuò)誤鏈的 Wrapping Error 概念,并同時(shí)增加 errors.Is 和 errors.As 的方法,與前面說(shuō)到的 Go1.13 的改進(jìn)一致,不再贅述。
需要留意的是,Go1.13 并沒(méi)有實(shí)現(xiàn) %+v 輸出調(diào)用堆棧的需求,因?yàn)榇伺e會(huì)破壞 Go1 兼容性和產(chǎn)生一些性能問(wèn)題,大概會(huì)在 Go2 加入。
try-catch 不香嗎
社區(qū)中另外一股聲音就是直指 Go 語(yǔ)言反人類不用 try-catch 的機(jī)制,在社區(qū)內(nèi)也產(chǎn)生了大量的探討,具體可以看看相關(guān)的提案 Proposal: A built-in Go error check function, "try"。
目前該提案已被拒絕,具體可參見(jiàn) go/issues/32437#issuecomment-512035919 和 Why does Go not have exceptions。
總結(jié)
在這篇文章中,我們介紹了目前 Go1 Error 的現(xiàn)狀,概括了大家對(duì) Go 語(yǔ)言錯(cuò)誤處理的常見(jiàn)問(wèn)題和意見(jiàn)。
同時(shí)還介紹了在這幾年間,Go team 針對(duì) Go2、Go1.13 Error 的持續(xù)優(yōu)化和探索。

















 
 
 





 
 
 
 