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

前言快一個月沒有更新技術文章了,這段時間投注了較多的時間學習字節的開源項目 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 anew error b*/wrapError 的定義wrapError 是嵌套的 error,也實現了 error 接口的 Error 方法,本質也是一個 error  , 并聲明了一個 Unwrap 方法用于拆包裝 。
type wrapError struct {msg stringerr 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 的創建邏輯 。
【Go 源碼解讀|如何用好 errors 庫的 errors.Is 與 errors.As() 方法】你也可以進一步去看 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")}/*打印結果:falseerr2 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 。

推薦閱讀