先睹爲快,Go2 Error 的掙扎之路

如有任何問題或建議,歡迎及時交流和碰撞。個人公衆號是 【腦子進煎魚了】,GitHub 地址: https://github.com/eddycjy

你們好,我是煎魚。html

自從 Go 語言在國內火熱以來,除去泛型,其次最具槽點的就是 Go 對錯誤的處理方式,一句經典的 if err != nil 暗號就能認出你是一個 Go 語言愛好者。git

image

天然,你們對 Go error 的關注度更是高漲,Go team 也是,所以在 Go 2 Draft Designs 中正式提到了 error handling(錯誤處理)的相關草案,但願可以在將來正式的解決這個問題。github

在今天這篇文章中,咱們將一同跟蹤 Go2 error,看看他是怎麼 「掙扎」 的,能不能破局?golang

爲何要吐槽 Go1

要吐槽 Go1 error,就得先知道爲何你們究竟是在噴 Error 哪裏處理的很差。在 Go 語言中,error 其實本質上只是個 Error 的 interfacejson

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 寫的煩,代碼中一大堆錯誤處理的判斷,佔了至關的比例,不夠優雅。
  • 在排查問題時,Go 的 err 並無其餘堆棧信息,只能本身增長描述信息,層層疊加,打一大堆日誌,排查很麻煩。
  • 在驗證和測試錯誤時,要自定義錯誤(各類判斷和斷言)或者被迫用字符串校驗。

Go1.13 的挽尊

在 2019 年 09 月,Go1.13 正式發佈。其中兩個比較大的兩個關注點分別是包依賴管理 Go modules 的轉正,以及錯誤處理 errors 標準庫的改進:

image

在本次改進中,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

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 錯誤鏈中識別和 target 相同的類型,若是能夠賦值,則返回 true。

errors.Unwarp

方法簽名:

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 方法。

民間自救 pkg/errors

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 進行了調整,但調用堆棧這塊因綜合緣由暫時沒有歸入。

Go2 error 要解決什麼問題

在前面咱們聊了 Go1 error 的許多問題,以及 Go1.13 和 pkg/errors 的自救和融合。你可能會疑惑,那...Go2 error 還有出場的機會嗎?即便 Go1 作了這些事情,Go1 error 還有問題嗎?

並無解決,if err != nil 依舊一把梭,目前社區聲音依然認爲 Go 語言的錯誤處理要改進。

Go2 error proposal

在 2018 年 8 月,官方正式公佈了 Go 2 Draft Designs,其中包含泛型和錯誤處理機制改進的初步草案:

image

注:Go1.13 正式將一些不破壞 Go1 兼容性的 Error 特性加入到了 main branch,也就是前面提到的 Wrapping Error。

錯誤處理(Error Handling)

第一個要解決的問題就是大量 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 Printing)

第二個要解決的問題是錯誤值(Error Values)、錯誤檢查(Error Inspection)的問題,其引伸出錯誤值打印(Error Printing)的問題,也能夠認爲是錯誤格式化的不便利。

官方針對此提出了提出了 Error ValuesError 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.Iserrors.As 的方法,與前面說到的 Go1.13 的改進一致,再也不贅述。

須要留意的是,Go1.13 並無實現 %+v 輸出調用堆棧的需求,由於此舉會破壞 Go1 兼容性和產生一些性能問題,大概會在 Go2 加入。

try-catch 不香嗎

社區中另一股聲音就是直指 Go 語言反人類不用 try-catch 的機制,在社區內也產生了大量的探討,具體能夠看看相關的提案 Proposal: A built-in Go error check function, "try"

目前該提案已被拒絕,具體可參見 go/issues/32437#issuecomment-512035919Why does Go not have exceptions

總結

在這篇文章中,咱們介紹了目前 Go1 Error 的現狀,歸納了你們對 Go 語言錯誤處理的常見問題和意見。同時還介紹了在這幾年間,Go team 針對 Go二、Go1.13 Error 的持續優化和探索。

若是是你,你會怎麼去優化目前 Go 語言的錯誤處理機制呢,如今 Go2 error proposal 你又是否定可?

參考

個人公衆號

分享 Go 語言、微服務架構和奇怪的系統設計,歡迎你們關注個人公衆號和我進行交流和溝通。

最好的關係是互相成就,各位的點贊就是煎魚創做的最大動力,感謝支持。

相關文章
相關標籤/搜索