• 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 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 仓库,正在更新中,你也可以从我往期的文章中看到一些说明。

  • 相关阅读:
    java后端请求过滤options方式,亲测有效
    vscode非常好用的扩展插件
    JS - 防抖与节流
    浅谈Lua协程和函数的尾调用
    [acwing周赛复盘] 第 60 场周赛20220716
    JPA框架
    java计算机毕业设计高校科研信息管理系统源码+mysql数据库+系统+lw文档+部署
    SQL如何从字符串截取指定字符(LEFT、MID、RIGHT三大函数)
    mybatis批量更新问题
    Java | abstract关键字【面向对象的第三大特征——多态】
  • 原文地址:https://www.cnblogs.com/YLTFY1998/p/16741285.html