天天看點

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

前言

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

www.cloudwego.io/

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

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

扯遠了,寫作這篇文章的原因是我在寫單元測試的時候,有時會涉及 ​

​errors.Is​

​​ 和 ​

​errors.As​

​ 方法的調用,借此做一個總結。

error 的定義

首先需要明确 Go 語言中的錯誤是通過接口定義的,是以是一個引用類型。

type error interface {
   Error() string
}
// Go 提供了一個預設實作
type errorString struct {
  s string
}

func (e *errorString) Error() string {
  return      

那麼如果我要建立一個 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      

通過 ​

​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      

核心就是 ​

​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      

如果這個 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 接口。

小結