Golang 語言怎麼處理錯誤?

介紹

golang 程序大多數是通過 if err != nil 處理錯誤,在 golang 社區中,有一部分 golang 程序員對此舉是持反對觀點,他們認爲在 golang 代碼中存在大量的錯誤處理代碼 if err != nil,使整體代碼變得非常不優雅,應該在 golang 中引入其他處理錯誤的機制,例如 try-catche 或其它此類處理錯誤的機制。其實,他們忽略了 golang 中一個特別重要的概念,即 errors are values,並且 golang 作者 Rob Pike 也對此問題做出過迴應,在 golang 代碼中出現重複的錯誤處理代碼 if err != nil,可能是 golang 用戶的使用方式有問題。

本文我們主要聊聊在 golang 中,怎麼處理錯誤?

golang 定義錯誤的兩種方式

使用 golang 標準庫 errors 的 New() 函數,可以定義一個錯誤類型的變量。

func New(text string) error

New() 函數接收一個 string 類型的文本,返回一個 error 類型的變量。即使給定的文本不同,每次對 New() 函數的調用也會返回不同的錯誤值。

關於每次調用 New() 函數,都可以返回不同的錯誤值,golang 是怎麼做到的呢?我們通過閱讀 golang 的源碼,找一下我們的問題答案。

源碼 /usr/local/go/src/errors/errors.go

// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error {
 return &errorString{text}
}

// errorString is a trivial implementation of error.
type errorString struct {
 s string
}

func (e *errorString) Error() string {
 return e.s
}

源碼中,我們發現 New() 函數體中的代碼是返回一個指針類型 &errorString{text},所以我們的疑問自然有了答案。

那麼,golang 中定義錯誤的另外一種方式是什麼?在 golang 標準庫 fmt 中,通過調用 Errorf() 函數也可以返回一個 error 類型的錯誤。

源碼 /usr/local/go/src/fmt/errors.go

func Errorf(format string, a ...interface{}) error {
 p := newPrinter()
 p.wrapErrs = true
 p.doPrintf(format, a)
 s := string(p.buf)
 var err error
 if p.wrappedErr == nil {
  err = errors.New(s)
 } else {
  err = &wrapError{s, p.wrappedErr}
 }
 p.free()
 return err
}

錯誤處理方式之 “不透明錯誤處理”

正如我們在文章開篇所述,在 golang 程序中,我們見的最多的錯誤處理方式就是 if err != nil,此種錯誤處理方式,錯誤處理方不關心錯誤提供方的錯誤值。因此,我們將此種錯誤處理方式稱爲 “不透明錯誤處理”。

示例代碼:

err := errors.New("this is a error example")
if err != nil {
 fmt.Println(err)
  return
}

golang 1.13 新增 As() 函數

在 golang 1.13 中,新增 As() 函數,當 error 類型的變量是一個包裝錯誤(wrap error)時,它可以順着錯誤鏈(error chain)上所有被包裝的錯誤(wrapped error)的類型做比較,直到找到一個匹配的錯誤類型,並返回 true,如果找不到,則返回 false。

通常,我們會使用 As() 函數判斷一個 error 類型的變量是否爲特定的自定義錯誤類型。

示例代碼:

// 自定義的錯誤類型
type DefineError struct {
 msg string
}

func (d *DefineError) Error() string {
 return d.msg
}

func main() {
  // wrap error
 err1 := &DefineError{"this is a define error type"}
 err2 := fmt.Errorf("wrap err2: %w\n", err1)
 err3 := fmt.Errorf("wrap err3: %w\n", err2)
 var err4 *DefineError
 if errors.As(err3, &err4) {
  // errors.As() 順着錯誤鏈,從 err3 一直找到被包裝最底層的錯誤值 err1,並且將 err3 與其自定義類型 `var err4 *DefineError` 匹配成功。
  fmt.Println("err1 is a variable of the DefineError type")
  fmt.Println(err4 == err1)
  return
 }
 fmt.Println("err1 is not a variable of the DefineError type")
}

golang 1.13 新增 Is() 函數

在 Part03 中,我們講述了 “不透明錯誤處理” 的錯誤處理方式,錯誤處理方不關心錯誤提供方的錯誤值。但是,在錯誤處理方需要關心錯誤提供方的錯誤值時,錯誤處理方要對錯誤提供方的錯誤值進行判定,這就造成了代碼的耦合,錯誤提供方的錯誤值每次修改,錯誤處理方都需要跟着做出相應修改。

針對這種情況,golang 一般會採用 “哨兵錯誤處理” 的錯誤處理方式,即定義可導出的錯誤變量,錯誤處理方和錯誤提供方都只操作錯誤變量,這樣做的好處是隻需維護錯誤變量,但是還沒有徹底解決問題,如果 error 類型的錯誤變量是一個包裝錯誤(wrap error),“哨兵錯誤處理”的錯誤處理方式也不方便處理該錯誤。

好在 golang 1.13 新增 Is() 函數,它可以順着錯誤鏈(error chain)上所有被包裝的錯誤(wrapped error)的類型做比較,直到找到一個匹配的錯誤。

示例代碼:

// 哨兵錯誤處理
var (
 ErrInvalidUser     = errors.New("invalid user")
 ErrNotFoundUser    = errors.New("not found user")
)

func main () {
  err1 := fmt.Errorf("wrap err1: %w\n", ErrInvalidUser)
 err2 := fmt.Errorf("wrap err2: %w\n", err1)
  // golang 1.13 新增 Is() 函數
 if errors.Is(err2, ErrInvalidUser) {
  fmt.Println(ErrInvalidUser)
  return
 }
 fmt.Println("success")
}

總結

本文我們開篇先是講述了 golang 社區中,存在對待 golang 錯誤處理方式的反對態度的用戶,這麼一個客觀事實。接着,我們介紹了 golang 中的兩種定義錯誤的方式和底層源碼實現,和 golang 1.13 中新增的關於錯誤處理的函數。如果你現在使用的是 golang 1.13 及以上版本,請使用 As() 和 Is() 。

通過閱讀源碼 /usr/local/go/src/errors/wrap.go,我們可以發現 As() 和 Is() 是通過在錯誤鏈中不斷調用 Unwrap() 函數,最終找到匹配的錯誤值。其中,Unwrap() 函數也是在 golang 1.13 中新增的函數。

Unwrap() 函數的源碼:

// Unwrap returns the result of calling the Unwrap method on err, if err's
// type contains an Unwrap method returning error.
// Otherwise, Unwrap returns nil.
func Unwrap(err error) error {
 u, ok := err.(interface {
  Unwrap() error
 })
 if !ok {
  return nil
 }
 return u.Unwrap()
}

參考資料:
https://golang.org/pkg/errors/

延伸閱讀:
https://blog.golang.org/error-handling-and-go
https://blog.golang.org/errors-are-values
https://blog.golang.org/go1.13-errors
https://golang.org/doc/tutorial/handle-errors
https://medium.com/rungo/error-handling-in-go-f0125de052f0
https://www.digitalocean.com/community/tutorials/handling-errors-in-go

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/o9E6NXLMCKndUZBNB2z1zA