關於 Golang 錯誤處理的思考

關於 Golang 錯誤處理的思考

Golang有不少優勢,這也是它如此流行的主要緣由。可是 Go 1 對錯誤處理的支持過於簡單了,以致於平常開發中會有諸多不便利,遭到不少開發者的吐槽。 這些不足催生了一些開源解決方案。與此同時, Go 官方也在從語言和標準庫層面做出改進。 這篇文章將給出幾種常見建立錯誤的方式並分析一些常見問題,對比各類解決方案,並展現了迄今爲止(go 1.13)的最佳實踐。git

幾種建立錯誤的方式

首先介紹幾種常見的建立錯誤的方法github

  1. 基於字符串的錯誤
err1 := errors.New("math: square root of negative number")

err2 := fmt.Errorf("math: square root of negative number %g", x)
複製代碼
  1. 帶有數據的自定義錯誤
package zError

import (
	"fmt"
	"github.com/satori/go.uuid"
	"log"
	"runtime/debug"
	"time"
)

type BaseError struct {
	InnerError error
	Message    string
	StackTrace string
	Misc       map[string]interface{}
}

func WrapError(err error, message string, messageArgs ...interface{}) BaseError {
	return BaseError{
		InnerError: err,
		Message:    fmt.Sprintf(message, messageArgs),
		StackTrace: string(debug.Stack()),
		Misc:       make(map[string]interface{}),
	}
}

func (err *BaseError) Error() string {
// 實現 Error 接口
	return err.Message
}
	
複製代碼

拋出問題

開發中常常須要檢查返回的錯誤值並做相應處理。下面給出一個最簡單的示例。golang

import (
   "database/sql"
   "fmt"
)

func GetSql() error {
   return sql.ErrNoRows
}

func Call() error {
   return GetSql()
}

func main() {
   err := Call()
   if err != nil {
      fmt.Printf("got err, %+v\n", err)
   }
}
//Outputs:
// got err, sql: no rows in result set
複製代碼

有時須要根據返回的error類型做不一樣處理,例如:sql

import (
   "database/sql"
   "fmt"
)

func GetSql() error {
   return sql.ErrNoRows
}

func Call() error {
   return GetSql()
}

func main() {
   err := Call()
   if err == sql.ErrNoRows {
      fmt.Printf("data not found, %+v\n", err)
      return
   }
   if err != nil {
      // Unknown error
   }
}
//Outputs:
// data not found, sql: no rows in result set
複製代碼

實踐中常常須要爲錯誤增長上下文信息後再返回,以方便調用者瞭解錯誤場景。例如 Getcall 方法時常寫成:ui

func Getcall() error {
   return fmt.Errorf("GetSql err, %v", sql.ErrNoRows)
}
複製代碼

不過這個時候 err == sql.ErrNoRows 就不成立了。除此以外,上述寫法都在返回錯誤時都丟掉了調用棧這個重要的信息。咱們須要更靈活、更通用的方式來應對此類問題。spa

解決方案

針對存在的不足,目前有幾種解決方案。這些方式能夠對錯誤進行上下文包裝,並攜帶原始錯誤信息, 還能儘可能保留完整的調用棧debug

方案 1: github.com/pkg/errors

  1. Wrap 方法用來包裝底層錯誤,增長上下文文本信息並附加調用棧。 通常用於包裝對第三方代碼(標準庫或第三方庫)的調用。
  2. WithMessage 方法僅增長上下文文本信息,不附加調用棧。 若是肯定錯誤已被 Wrap 過或不關心調用棧,可使用此方法。 注意:不要反覆 Wrap ,會致使調用棧重複
  3. Cause方法用來判斷底層錯誤 。

如今咱們用這三個方法來重寫上面的代碼:code

import (
   "database/sql"
   "fmt"

   "github.com/pkg/errors"
)

func GetSql() error {
   return errors.Wrap(sql.ErrNoRows, "GetSql failed")
}

func Call() error {
   return errors.WithMessage(GetSql(), "bar failed")
}

func main() {
   err := Call()
   if errors.Cause(err) == sql.ErrNoRows {
      fmt.Printf("data not found, %v\n", err)
      fmt.Printf("%+v\n", err)
      return
   }
   if err != nil {
      // unknown error
   }
}
/*Output: data not found, Call failed: GetSql failed: sql: no rows in result set sql: no rows in result set main.GetSql /usr/three/main.go:11 main.Call /usr/three/main.go:15 main.main /usr/three/main.go:19 runtime.main ... */

複製代碼

從輸出內容能夠看到, 使用 %v 做爲格式化參數,那麼錯誤信息會保持一行, 其中依次包含調用棧的上下文文本。 使用 %+v ,則會輸出完整的調用棧詳情。 若是不須要增長額外上下文信息,僅附加調用棧後返回,可使用 WithStack 方法:繼承

func GetSql() error {
   return errors.WithStack(sql.ErrNoRows)
}
複製代碼

注意:不管是 Wrap , WithMessage 仍是 WithStack ,當傳入的 err 參數爲 nil 時, 都會返回nil, 這意味着咱們在調用此方法以前無需做 nil 判斷,保持了代碼簡潔接口

方案 2:golang.org/x/xerrors

結合社區反饋,Go 團隊完成了在 Go 2 中簡化錯誤處理的提案。 Go核心團隊成員 Russ Cox 在xerrors中部分實現了提案中的內容。它用與 github.com/pkg/errors類似的思路解決同一問題, 引入了一個新的 fmt 格式化動詞: %w,使用 Is 進行判斷。

import (
   "database/sql"
   "fmt"

   "golang.org/x/xerrors"
)

func Call() error {
   if err := GetSql(); err != nil {
      return xerrors.Errorf("bar failed: %w", GetSql())
   }
   return nil
}

func GetSql() error {
   return xerrors.Errorf("GetSql failed: %w", sql.ErrNoRows)
}

func main() {
   err := Call()
   if xerrors.Is(err, sql.ErrNoRows) {
      fmt.Printf("data not found, %v\n", err)
      fmt.Printf("%+v\n", err)
      return
   }
   if err != nil {
      // unknown error
   }
}
/* Outputs: data not found, Call failed: GetSql failed: sql: no rows in result set bar failed: main.Call /usr/four/main.go:12 - GetSql failed: main.GetSql /usr/four/main.go:18 - sql: no rows in result set */
複製代碼

與 github.com/pkg/errors 相比,它有幾點不足:

  1. 使用 : %w 代替了 Wrap , 看似簡化, 但失去了編譯期檢查。 若是沒有冒號,或 : %w 不位於于格式化字符串的結尾,或冒號與百分號之間沒有空格,包裝將失效且不報錯;
  2. 並且,調用 xerrors.Errorf 以前須要對參數進行nil判斷。 這徹底沒有簡化開發者的工做

方案 3:Go 1.13 內置支持

Go 1.13 將 xerrors 的部分功能(不是所有)整合進了標準庫。 它繼承了上面提到的 xerrors 的所有缺點, 並額外貢獻了一項。所以目前沒有使用它的必要。

import (
   "database/sql"
   "errors"
   "fmt"
)

func Call() error {
   if err := GetSql(); err != nil {
      return fmt.Errorf("Call failed: %w", GetSql())
   }
   return nil
}

func GetSql() error {
   return fmt.Errorf("GetSql failed: %w", sql.ErrNoRows)
}

func main() {
   err := Call()
   if errors.Is(err, sql.ErrNoRows) {
      fmt.Printf("data not found, %+v\n", err)
      return
   }
   if err != nil {
      // unknown error
   }
}
/* Outputs: data not found, Call failed: GetSql failed: sql: no rows in result set */
複製代碼

上面的代碼與 xerrors 版本很是接近。可是它不支持調用棧信息輸出, 根據官方的說法, 此功能沒有明確的支持時間。所以其實用性遠低於 github.com/pkg/errors。

得出結論

經過以上對比, 相信你已經有了選擇。 再明確一下個人見解,若是你正在使用 github.com/pkg/errors ,那就保持現狀吧。目前尚未比它更好的選擇。若是你已經大量使用 golang.org/x/xerrors , 別盲目換成 go 1.13 的內置方案。 總的來講,Go 在誕生之初就在各個方面表現得至關成熟、穩健。 在演進路線上不多出現猶疑和搖擺, 而在錯誤處理方面倒是個例外。 除了被普遍吐槽的 if err != nil 以外, 就連其改進路線也備受爭議、分歧明顯,以至於一個改進提案都會由於壓倒性的反對意見而不得不做出調整。 好在 Go 團隊比之前更加樂於傾聽社區意見,團隊甚至專門就此問題建了個反饋收集頁面。相信最終你們會找到更好的解決方案。

相關文章
相關標籤/搜索