快一个月没有更新技术文章了,这段时间投注了较多的时间学习字节的开源项目 Kitex/Hertz ,并维护一些简单的 issue ,有兴趣的同学也可以去了解:
这段时间迟迟没有更新文章,一方面是接触到了很多大佬,反观自身技术深度远远不及,变得不敢轻易下笔;另一方面反思了一下自己之前的写作,确实也有一些功利的成分,有时为了更新而更新,打算纠正。
接触开源之后,我感受到了开源社区打磨一个项目的认真与严谨,后续也希望自己能以此为鉴,对开源、对写作都是如此。
扯远了,写作这篇文章的原因是我在写单元测试的时候,有时会涉及 errors.Is
和 errors.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
方法,则调用其逻辑,否则也是走递归拆包的逻辑。
Original: https://www.cnblogs.com/YLTFY1998/p/16741285.html
Author: 白泽来了
Title: Go 源码解读|如何用好 errors 库的 errors.Is() 与 errors.As() 方法
原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/801689/
转载文章受原作者版权保护。转载请注明原作者出处!