golang優雅的錯誤處理

golang的錯誤處理一直深受大家詬病,項目裏面一半的代碼在做錯誤處理。

自己在做golang開發一段時間後,也深有同感,覺得很有必要優化一下,一方面讓代碼更優雅一些,另一方面也爲了形成系統的錯誤處理方式,而不是隨心所欲的來個errors.new(),或者一直return err。

在查閱一些資料之後,發現自己對golang錯誤處理的認識,還停留在一個低階的層面上。這裏想和大家探討一下,也爲鞏固自己所學

錯誤的返回處理

在函數多層調用時,我常用的處理方式是:

func Write(w io.Writer, buf []byte) error {
  _, err := w.Write(buf)
  if err != nil {
    // annotated error goes to log file
    log.Println("unable to write:", err)
  
    // unannotated error returned to caller
    return err
  }
  return nil
}

層層都加日誌非常方便故障定位,但這樣做,日誌文件中會有很多重複的錯誤描述,並且上層調用函數拿到的錯誤,還是底層函數返回的 error,沒有上下文信息

優化一:

func Write(w io.Writer, buf []byte) error {
  _, err := w.Write(buf)
  if err != nil {
    // annotated error returned to caller
    fmt.Errorf("authenticate failed: %v", err)
  }
  return nil
}

這裏去除了重複的錯誤日誌,並且在返回給上層調用函數的error信息中加上了上下文信息。但是這樣做破壞了相等性檢測,即我們無法判斷錯誤是否是一種預先定義好的錯誤。

例如:

func main() {
    err := readfile(“.bashrc”)
    if strings.Contains(error.Error(), "not found") {
        // handle error
    }
}

func readfile(path string) error {
    err := openfile(path)
    if err != nil {
        return fmt.Errorf(“cannot open file: %v", err)
    }
    // ……
}

造成的後果時,調用者不得不用字符串匹配的方式判斷底層函數 readfile 是不是出現了某種錯誤。

優化二:

使用第三方庫: github.com/pkg/errors,wrap可以將一個錯誤加上一段字符串,包裝成新的字符串。cause進行相反的操作。

// Wrap annotates cause with a message.
func Wrap(cause error, message string) error
// Cause unwraps an annotated error.
func Cause(err error) error

例如:

func ReadFile(path string) ([]byte, error) {
    f, err := os.Open(path)
    if err != nil {
        return nil, errors.Wrap(err, "open failed")
    }
    defer f.Close()
    
    buf, err := ioutil.ReadAll(f)
    if err != nil {
        return nil, errors.Wrap(err, "read failed")
    }
    return buf, nil
}

通過wrap即可以包含底層被調用函數的上下文信息,又可以通過cause還原錯誤,對原錯誤類型進行判斷,如下:

func main() {
    _, err := ReadFile()
    if errors.Cause(err) != io.EOF {
        fmt.Println(err)
        os.Exit(1)
    }
}

今年剛發佈的go1.13新增了類似的錯誤處理函數

//go1.13 沒有提供wrap函數,但通過fmt.Errof提供了類似的功能
fmt.Errorf("context info: %w",err)

//將嵌套的 error 解析出來,多層嵌套需要調用 Unwrap 函數多次,才能獲取最裏層的 error
func Unwrap(err error) error

異常

部分開發者寫代碼中,沒有區分異常和錯誤,都統一按錯誤來處理,這種方式是不優雅的。要靈活使用Golang的內置函數panic和recover來觸發和終止異常處理流程。

錯誤指的是可能出現問題的地方出現了問題,比如打開一個文件時失敗,這種情況在人們的意料之中 ;而異常指的是不應該出現問題的地方出現了問題,比如引用了空指針,這種情況在人們的意料之外。

這裏給出一些應拋出異常的場景:

  1. 空指針引用
  2. 下標越界
  3. 除數爲0
  4. 不應該出現的分支,比如default
  5. 輸入不應該引起函數錯誤

在應用開發過程中,通過拋出panic異常,程序退出,及時發現問題。在部署以後,要保證程序的持續穩定運行,需要及時通過recover捕獲異常。在recover中,要用合理的方式處理異常,如:

  1. 打印堆棧的調用信息和業務信息,方便記錄和排查問題
  2. 將異常轉換爲錯誤,返回給上層調用者處理

例如:

func funcA() (err error) {
    defer func() {
        if p := recover(); p != nil {
            fmt.Println("panic recover! p:", p)
            str, ok := p.(string)
            if ok {
                err = errors.New(str)
            } else {
                err = errors.New("panic")
            }
            debug.PrintStack()
        }
    }()
    return funcB()
}

func funcB() error {
    // simulation
    panic("foo")
    return errors.New("success")
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章