Go 原始碼解讀|如何用好 errors 庫的 errors.Is() 與 errors.As() 方法

2022-09-29 15:01:02

前言

快一個月沒有更新技術文章了,這段時間投注了較多的時間學習位元組的開源專案 Kitex/Hertz ,並維護一些簡單的 issue ,有興趣的同學也可以去了解:

https://www.cloudwego.io/

這段時間遲遲沒有更新文章,一方面是接觸到了很多大佬,反觀自身技術深度遠遠不及,變得不敢輕易下筆;另一方面反思了一下自己之前的寫作,確實也有一些功利的成分,有時為了更新而更新,打算糾正。

接觸開源之後,我感受到了開源社群打磨一個專案的認真與嚴謹,後續也希望自己能以此為鑑,對開源、對寫作都是如此。

扯遠了,寫作這篇文章的原因是我在寫單元測試的時候,有時會涉及 errors.Iserrors.As 方法的呼叫,藉此做一個總結。

error 的定義

首先需要明確 Go 語言中的錯誤是通過介面定義的,因此是一個參照型別。

type error interface {
   Error() string
}
// Go 提供了一個預設實現
type errorString struct {
  s string
}
​
func (e *errorString) Error() string {
  return e.s
}

那麼如果我要建立一個 error 範例,可以選擇下面的方式:

func main() {
   // 此時建立的兩個 error 都是 errorString 結構型別的
   errA := errors.New("new error a")
   fmt.Println(errA)
   errB := fmt.Errorf("new error %s", "b")
   fmt.Println(errB)
}
/*
  列印結果:
  new error a
  new error b
*/

wrapError 的定義

wrapError 是巢狀的 error ,也實現了 error 介面的 Error 方法,本質也是一個 error ,並宣告了一個 Unwrap 方法用於拆包裝。

type wrapError struct {
  msg string
  err error
}
​
func (e *wrapError) Error() string {
  return e.msg
}
​
func (e *wrapError) Unwrap() error {
  return e.err
}

通過 fmt.Errorf 方法配合 %w 預留位置建立巢狀型別的 wrapError。

var BaseErr = errors.New("the underlying base error")
​
func main() {
  err1 := fmt.Errorf("wrap base: %w", BaseErr)
  fmt.Println(err1)
  err2 := fmt.Errorf("wrap err1: %w", err1)
  fmt.Println(err2)
}
/*
  列印結果:
  wrap base: the underlying base error
  wrap err1: wrap base: the underlying base error
*/

為什麼 fmt.Errorf 用了預留位置 %w 之後建立的就是 wrapError 型別,而用了 fmt.Errorf 但只是選擇其他預留位置如上述範例中的 %s 建立的就是 errorString 型別?

可以簡單看一下 fmt.Errorf 方法的原始碼:

func Errorf(format string, a ...any) 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
}

核心就是 p.doPrintf(format, a) 呼叫後,如果包含 %w 預留位置則會先建立內層的 error ,賦值給 p.wrappedErr ,從而觸發 wrapError 的建立邏輯。

你也可以進一步去看 p.doPrintf(format, a) 的實現印證這個流程。

errors.Is

判斷被包裝的error是否包含指定錯誤。

var BaseErr = errors.New("the underlying base error")
​
func main() {
   err1 := fmt.Errorf("wrap base: %w", BaseErr)
   err2 := fmt.Errorf("wrap err1: %w", err1)
   println(err2 == BaseErr) // false
   if !errors.Is(err2, BaseErr) {
      panic("err2 is not BaseErr")
   }
   println("err2 is BaseErr")
}
/*
  列印結果:
  false
  err2 is BaseErr
*/

來看一下 errors.Is 方法的原始碼:

func Is(err, target error) bool {
   if target == nil {
      return err == target
   }
​
   isComparable := reflectlite.TypeOf(target).Comparable()
   for {
      if isComparable && err == target {
         return true
      }
      if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
         return true
      }
      if err = Unwrap(err); err == nil {
         return false
      }
   }
}
​
func Unwrap(err error) error {
  u, ok := err.(interface {
    Unwrap() error
  })
  if !ok {
    return nil
  }
  return u.Unwrap()
}

如果這個 err 自己實現了 interface{ Is(error) bool } 介面,通過介面斷言,可以呼叫 Is 方法判斷 err 是否與 target 相等。

否則遞迴呼叫 Unwrap 方法拆包裝,返回下一層的 error 去判斷是否與 target 相等。

errors.As

提取指定型別的錯誤,判斷包裝的 error 鏈中,某一個 error 的型別是否與 target 相同,並提取第一個符合目標型別的錯誤的值,將其賦值給 target。

type TypicalErr struct {
   e string
}
​
func (t TypicalErr) Error() string {
   return t.e
}
​
func main() {
   err := TypicalErr{"typical error"}
   err1 := fmt.Errorf("wrap err: %w", err)
   err2 := fmt.Errorf("wrap err1: %w", err1)
   var e TypicalErr
   if !errors.As(err2, &e) {
      panic("TypicalErr is not on the chain of err2")
   }
   println("TypicalErr is on the chain of err2")
   println(err == e)
}
/*
  列印結果:
  TypicalErr is on the chain of err2
  true
*/

來看一下 error.As 方法的原始碼:

func As(err error, target any) bool {
   if target == nil {
      panic("errors: target cannot be nil")
   }
   val := reflectlite.ValueOf(target)
   typ := val.Type()
   if typ.Kind() != reflectlite.Ptr || val.IsNil() {
      panic("errors: target must be a non-nil pointer")
   }
   targetType := typ.Elem()
   if targetType.Kind() != reflectlite.Interface && !targetType.Implements(errorType) {
      panic("errors: *target must be interface or implement error")
   }
   for err != nil {
      if reflectlite.TypeOf(err).AssignableTo(targetType) {
         val.Elem().Set(reflectlite.ValueOf(err))
         return true
      }
      if x, ok := err.(interface{ As(any) bool }); ok && x.As(target) {
         return true
      }
      err = Unwrap(err)
   }
   return false
}

原始碼 for 迴圈前的部分是用來約束 target 引數的型別,要求其是一個非空的指標型別。

此外要求 *target 是一個介面或者實現了 error 介面。

for 迴圈判斷 err 是否可以賦值給 target 所屬型別,如果可以則賦值返回 true。

如果 err 實現了自己的 As 方法,則呼叫其邏輯,否則也是走遞迴拆包的邏輯。

小結

後續將繼續分享一些原始碼解讀的文章,關於 Go 語言的學習,我也開源了一個 GitHub 倉庫,正在更新中,你也可以從我往期的文章中看到一些說明。