如有任何問題或建議,歡迎及時交流和碰撞。個人公衆號是 【腦子進煎魚了】,GitHub 地址: https://github.com/eddycjy。
你們好,我是煎魚。html
自從 Go 語言在國內火熱以來,除去泛型,其次最具槽點的就是 Go 對錯誤的處理方式,一句經典的 if err != nil
暗號就能認出你是一個 Go 語言愛好者。git
天然,你們對 Go error 的關注度更是高漲,Go team 也是,所以在 Go 2 Draft Designs 中正式提到了 error handling(錯誤處理)的相關草案,但願可以在將來正式的解決這個問題。github
在今天這篇文章中,咱們將一同跟蹤 Go2 error,看看他是怎麼 「掙扎」 的,能不能破局?golang
要吐槽 Go1 error,就得先知道爲何你們究竟是在噴 Error 哪裏處理的很差。在 Go 語言中,error 其實本質上只是個 Error 的 interface
:json
type error interface { Error() string }
實際的應用場景以下:架構
func main() { x, err := foo() if err != nil { // handle error } }
單純的看這個例子彷佛沒什麼問題,但工程大了後呢?顯然 if err != nil
的邏輯是會堆積在工程代碼中,Go 代碼裏的 if err != nil
甚至會達到工程代碼量的 30% 以上:app
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 } }
暴力的對比一下,就發現四行函數調用,十二行錯誤,還要苦練且精通 IDE 的快速摺疊功能,仍是比較麻煩的。函數
另外既然是錯誤處理,那確定不僅僅是一個 return err
了。在工程實踐中,項目代碼都是層層嵌套的,若是直接寫成:微服務
if err != nil { return err }
在實際工程中確定是不行。你怎麼知道具體是哪裏拋出來的錯誤信息,實際出錯時只能瞎猜。你們又想出了 PlanB,那就是加各類描述信息:oop
if err != nil { logger.Errorf("煎魚報錯 err:%v", err) return err }
雖然看上去人模人樣的,在實際出錯時,也會遇到新的問題,由於你要去查這個錯誤是從哪裏拋出來的,單純幾句錯誤描述是難以定位的。這時候就會發展成處處打錯誤日誌:
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 }
雖然處處打了日誌,就會變成錯誤日誌很是多,一旦出問題,人肉可能短期內識別不出來。且最多見的就是到 IDE 上 ctrl + f
搜索是在哪出錯,同時在咱們經常會自定義一些錯誤類型,而在 Go 則須要各類判斷和處理:
if err := dec.Decode(&val); err != nil { if serr, ok := err.(*json.SyntaxError); ok { ... } return err }
首先你得判斷不等於 nil
,還得對自定義的錯誤類型進行斷言,總體來說比較繁瑣。
彙總來說,Go1 錯誤處理的問題至少有:
if err != nil
寫的煩,代碼中一大堆錯誤處理的判斷,佔了至關的比例,不夠優雅。err
並無其餘堆棧信息,只能本身增長描述信息,層層疊加,打一大堆日誌,排查很麻煩。在 2019 年 09 月,Go1.13 正式發佈。其中兩個比較大的兩個關注點分別是包依賴管理 Go modules 的轉正,以及錯誤處理 errors 標準庫的改進:
在本次改進中,errors 標準庫引入了 Wrapping Error 的概念,並增長了 Is/As/Unwarp 三個方法,用於對所返回的錯誤進行二次處理和識別。同時也是將 Go2 error 預規劃中沒有破壞 Go1 兼容性的相關功能提早實現了。
簡單來說,Go1.13 後 Go 的 error 就能夠嵌套了,並提供了三個配套的方法。例子:
func main() { e := errors.New("腦子進煎魚了") w := fmt.Errorf("快抓住:%w", e) fmt.Println(w) fmt.Println(errors.Unwrap(w)) }
輸出結果:
$ go run main.go 快抓住:腦子進煎魚了 腦子進煎魚了
在上述代碼中,變量 w
就是一個嵌套一層的 error。最外層是 「快抓住:」,此處調用 %w
意味着 Wrapping Error 的嵌套生成。所以最終輸出了 「快抓住:腦子進煎魚了」。
須要注意的是,Go 並無提供 Warp
方法,而是直接擴展了 fmt.Errorf
方法。而下方的輸出因爲直接調用了 errors.Unwarp
方法,所以將 「取」 出一層嵌套,最終直接輸出 「腦子進煎魚了」。
對 Wrapping Error 有了基本理解後,咱們簡單介紹一下三個配套方法:
func Is(err, target error) bool func As(err error, target interface{}) bool func Unwrap(err error) error
方法簽名:
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。
方法簽名:
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 錯誤鏈中識別和 target 相同的類型,若是能夠賦值,則返回 true。
方法簽名:
func Unwrap(err error) error
方法例子:
func main() { e := errors.New("腦子進煎魚了") w := fmt.Errorf("快抓住:%w", e) fmt.Println(w) fmt.Println(errors.Unwrap(w)) }
該方法的做用是將嵌套的 error 解析出來,若存在多級嵌套則須要調用屢次 Unwarp 方法。
Go1 的 error 處理當然存在許多問題,所以在 Go1.13 前,早已有 「民間」 發現沒有上下文調試信息在實際工程應用中存在嚴重的體感問題。所以 github.com/pkg/errors
在 2016 年誕生了,目前該庫也已經受到了極大的關注。
官方例子以下:
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
簡單來說,就是對 Go1 error 的上下文處理進行了優化和處理,例如類型斷言、調用堆棧等。如有興趣的小夥伴能夠自行到 github.com/pkg/errors
進行學習。
另外你可能會發現 Go1.13 新增的 Wrapping Error 體系與 pkg/errors
有些相像。你並無體會錯,Go team 接納了相關的意見,對 Go1 進行了調整,但調用堆棧這塊因綜合緣由暫時沒有歸入。
在前面咱們聊了 Go1 error 的許多問題,以及 Go1.13 和 pkg/errors
的自救和融合。你可能會疑惑,那...Go2 error 還有出場的機會嗎?即便 Go1 作了這些事情,Go1 error 還有問題嗎?
並無解決,if err != nil
依舊一把梭,目前社區聲音依然認爲 Go 語言的錯誤處理要改進。
在 2018 年 8 月,官方正式公佈了 Go 2 Draft Designs,其中包含泛型和錯誤處理機制改進的初步草案:
注:Go1.13 正式將一些不破壞 Go1 兼容性的 Error 特性加入到了 main branch,也就是前面提到的 Wrapping Error。
第一個要解決的問題就是大量 if err != nil
的問題,針對此提出了 Go2 error handling 的草案設計。
簡單例子:
if err != nil { return err }
優化後的方案以下:
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 }
主函數:
func main() { handle err { log.Fatal(err) } hex := check ioutil.ReadAll(os.Stdin) data := check parseHexdump(string(hex)) os.Stdout.Write(data) }
該提案引入了兩種新的語法形式,首先是 check
關鍵字,其能夠選中一個表達式 check f(x, y, z)
或 check err
,其將會標識這是一個顯式的錯誤檢查。
其次引入了 handle
關鍵字,用於定義錯誤處理程序流轉,逐級上拋,依此類推,直處處理程序執行 return
語句,才正式結束。
第二個要解決的問題是錯誤值(Error Values)、錯誤檢查(Error Inspection)的問題,其引伸出錯誤值打印(Error Printing)的問題,也能夠認爲是錯誤格式化的不便利。
官方針對此提出了提出了 Error Values 和 Error Printing 的草案設計。
簡單例子以下:
if err != nil { return fmt.Errorf("write users database: %v", err) }
優化後的方案以下:
package errors type Wrapper interface { Unwrap() error } func Is(err, target error) bool func As(type E)(err error) (e E, ok bool)
該提案增長了錯誤鏈的 Wrapping Error 概念,並同時增長 errors.Is
和 errors.As
的方法,與前面說到的 Go1.13 的改進一致,再也不贅述。
須要留意的是,Go1.13 並無實現 %+v
輸出調用堆棧的需求,由於此舉會破壞 Go1 兼容性和產生一些性能問題,大概會在 Go2 加入。
社區中另一股聲音就是直指 Go 語言反人類不用 try-catch
的機制,在社區內也產生了大量的探討,具體能夠看看相關的提案 Proposal: A built-in Go error check function, "try"。
目前該提案已被拒絕,具體可參見 go/issues/32437#issuecomment-512035919 和 Why does Go not have exceptions。
在這篇文章中,咱們介紹了目前 Go1 Error 的現狀,歸納了你們對 Go 語言錯誤處理的常見問題和意見。同時還介紹了在這幾年間,Go team 針對 Go二、Go1.13 Error 的持續優化和探索。
若是是你,你會怎麼去優化目前 Go 語言的錯誤處理機制呢,如今 Go2 error proposal 你又是否定可?
分享 Go 語言、微服務架構和奇怪的系統設計,歡迎你們關注個人公衆號和我進行交流和溝通。
最好的關係是互相成就,各位的點贊就是煎魚創做的最大動力,感謝支持。