【golang】24、fmt.Errorf(), error.Is() 和 error.As()


一、默认的 error 实现

首先,go 定义了 error interface,并提供了默认的实现

type error interface {
    Error() string
}

// 默认的实现
typr errorString struct {
    s string
}
func (e *errorString) Error() String {
    return e.s
}

二、自定义 error 实现

那么,如果想自定义 error 类型,则可通过如下两种方法:

func main() {
    errA := errors.New("new error a");
    fmt.Println(errA)
    
    errB := fmt.Errorf("new error %s", "b");
    fmt.Println(errB)
}

// go run main.go
new error a
new error b

2.1 通过 fmt.Errorf(%w) 创建嵌套 error

wrapError 是官方定义的,嵌套的 error,其也实现了 error interface,并声明了一个 Unwrap() 方法来拆包装。

定义如下:

type wrapError struct {
    msg string
    err error
}

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

使用方法如下:

var baseErr = errors.New("baseErr");
func main() {
    err1 := fmt.Errorf("wrap base: %w", baseErr);
    fmt.Println(err1)
    
    err2 := fmt.Errorf("wrap err1: %w", err1);
    fmt.Println(err2)
}

// go run
wrap base: baseErr
wrap err1: wrap base: baseErr

2.1.1 fmt.Errorf() 实现

为什么 fmt.Errorf 用了占位符 %w 之后创建的就是 wrapError 类型,而用了 fmt.Errorf 但只是选择其他占位符如上述示例中的 %s 创建的就是 errorString 类型?

可以简单看一下 fmt.Errorf 方法的源码:

// Errorf formats according to a format specifier and returns the string as a
// value that satisfies error.
//
// If the format specifier includes a %w verb with an error operand,
// the returned error will implement an Unwrap method returning the operand.
// If there is more than one %w verb, the returned error will implement an
// Unwrap method returning a []error containing all the %w operands in the
// order they appear in the arguments.
// It is invalid to supply the %w verb with an operand that does not implement
// the error interface. The %w verb is otherwise a synonym for %v.
func Errorf(format string, a ...any) error {
  // 先申请一个 pp struct, 其中有 buf buffer、wrapErrs bool 和 wrappedErrs []int 等属性
  /*
  // pp is used to store a printer's state and is reused with sync.Pool to avoid allocations.
  type pp struct {
      buf buffer
      ...
      // wrapErrs is set when the format string may contain a %w verb.
      wrapErrs bool
      // wrappedErrs records the targets of the %w verb.
      wrappedErrs []int
  }
  */
  p := newPrinter()
  
  // 手动设置 wrapErrs, 表示 format 可能含有 %w 谓词
  p.wrapErrs = true
  
  // 遍历 format 字符串的每个字符
  // 如果其中含有 %w 则通过 p.wrappedErrs = append(p.wrappedErrs, argNum) 追加 %w 的下标位置到 p.wrappedErrs 数组中
  p.doPrintf(format, a)
  
  s := string(p.buf) // s = p.buf 为输入的内容
  var err error
  
  // 判断 p.wrappedErrs 数组的长度
  switch len(p.wrappedErrs) {
  case 0: // 数组长度为 0,则意为没有 %w 谓词,则直接新建一个 errorString struct
   err = errors.New(s)
  case 1: // 数组长度为 1,则意为有一个 %w 谓词,最终会返回 wrapError struct, 它只包含一个 err error
  /*
    type wrapError struct {
      msg string
      err error
    }
    
    func (e *wrapError) Error() string {
      return e.msg
    }
    
    func (e *wrapError) Unwrap() error {
      return e.err
    }
  */
   w := &wrapError{msg: s} // 封装 wrapError struct,使其 msg 属性为 s
   // a[p.wrappedErrs[0]]`表示通过`%w`动词传递给`Errorf`函数的第一个错误操作数。其中 p.wrappedErrs 是参数下标数组,p.wrappedErrs[0] 是参数下标,a 是参数数组,a[p.wrappedErrs[0]] 是第一个参数的值
   // 然后,使用类型断言`(error)`将这个操作数转换为`error`类型
   // 并将其赋值给`w.err`。这样,`w.err`就包含了通过`%w`传递给`Errorf`函数的第一个错误操作数。
   w.err, _ = a[p.wrappedErrs[0]].(error)
   err = w
  default: // 数组长度大于 1,则意为有大于一个 %w 谓词,最终会返回 wrapErrors struct, 它只包含数组 errs []error。总结起来,它会对参数进行排序并创建一个包含所有`%w`操作数的`[]error`切片的错误对象。
  /*
    type wrapErrors struct {
      msg  string
      errs []error
    }
    
    func (e *wrapErrors) Error() string {
      return e.msg
    }
    
    func (e *wrapErrors) Unwrap() []error {
      return e.errs
    }
  */
  // 在这种情况下,返回的错误对象将实现一个`Unwrap`方法,该方法返回一个包含所有`%w`操作数的`[]error`切片,按照它们在参数中出现的顺序进行排序。

   // 首先检查`p.reordered`属性是否为`true`。如果为`true`,则对`p.wrappedErrs`数组进行排序。
   if p.reordered {
    sort.Ints(p.wrappedErrs)
   }
   
   // 然后,创建一个切片`errs`,用于存储转换为`error`类型的参数值。
   var errs []error
   
   // 接下来,使用一个循环遍历`p.wrappedErrs`数组中的每个元素。
   for i, argNum := range p.wrappedErrs {
    // 在每次迭代中,检查当前元素是否与前一个元素相同。如果相同,则跳过当前迭代。
    if i > 0 && p.wrappedErrs[i-1] == argNum {
     continue
    }
    // 然后,检查对应参数下标的元素值是否实现了`error`接口。如果是,则将其转换为`error`类型,并将其追加到`errs`切片中。
    if e, ok := a[argNum].(error); ok {
     errs = append(errs, e)
    }
   }
   
   // 最后,创建一个`wrapErrors`结构体,将格式化后的字符串`s`和`errs`切片作为其字段值,并将该结构体赋值给变量`err`。
   err = &wrapErrors{s, errs}
  }
  p.free()
  return err
}

// 单 err
type wrapError struct {
  msg string
  err error
}

func (e *wrapError) Error() string {
  return e.msg
}

func (e *wrapError) Unwrap() error {
  return e.err
}

// 多 errs
type wrapErrors struct {
  msg  string
  errs []error
}

func (e *wrapErrors) Error() string {
  return e.msg
}

func (e *wrapErrors) Unwrap() []error {
  return e.errs
}

2.2 errors.Is()

作用:判断被包装过的error,是否包含指定错误

2.2.1 使用

package main

import (
    "errors"
    "fmt"
)

var baseErr = errors.New("baseErr")   // 是 errorString struct 类型
var baseErr1 = errors.New("baseErr1") // 是 errorString struct 类型

func main() {
    err1 := fmt.Errorf("wrap base: %w", baseErr) // 是 wrapError struct 类型
    err2 := fmt.Errorf("wrap err1: %w", err1)    // 是 wrapError struct 类型

    // 比较 baseErr1 和 baseErr
    if baseErr1 == baseErr {
       println("[use = ] baseErr1 == baseErr")
    } else {
       // 首先,通过使用"="运算符比较baseErr1和baseErr,结果是不相等。这是因为"="运算符在比较错误变量时比较的是它们的指针地址,而不是它们的值。
       // 尽管baseErr和baseErr1的文本内容相同,但它们是不同的错误变量,因此它们的指针地址也不同。因此打印的结果是 [use = ] baseErr1 != baseErr
       println("[use = ] baseErr1 != baseErr")
    }
    if errors.Is(baseErr1, baseErr) {
       println("[use is] baseErr1 is baseErr")
    } else {
       // 接下来,使用errors.Is()函数比较baseErr1和baseErr。这个函数的作用是检查错误链中是否存在目标错误。
       // 在这种情况下,baseErr1并没有直接包含baseErr,因此返回的结果是错误的。因此打印的结果是"[use is] baseErr1 is not baseErr"。
       println("[use is] baseErr1 is not baseErr")
    }

    // 比较 err2 和 baseErr
    if err2 == baseErr {
       println("[use = ] err2 == baseErr")
    } else {
       // 通过使用"="运算符比较err2和baseErr,结果同样是不相等。尽管err2通过fmt.Errorf()函数包装了baseErr,但它们仍然是不同的错误变量,因此指针地址不同。因此打印的结果是 "[use = ] err2 != baseErr"
       println("[use = ] err2 != baseErr")
    }
    if errors.Is(err2, baseErr) {
       // errors.Is()函数会遍历错误链,检查是否存在目标错误。在这种情况下,err2的错误链中包含了baseErr,因此返回的结果是正确的。因此打印的结果是"[use is] err2 is baseErr"。
       println("[use is] err2 is baseErr")
    } else {
       println("[use is] err2 is not baseErr")
    }
}

// go run main.go
[use = ] baseErr1 != baseErr
[use is] baseErr1 is not baseErr
[use = ] err2 != baseErr
[use is] err2 is baseErr

如果这个 err 实现了 interface{ Is(error) bool } 接口,通过接口断言,可以调用 Is 方法判断 err 是否与 target 相等。否则递归调用 Unwrap 方法拆包装,返回下一层的 error 去判断是否与 target 相等。

2.2.2 实现

// Is reports whether any error in err's tree matches target.
//
// The tree consists of err itself, followed by the errors obtained by repeatedly
// calling its Unwrap() error or Unwrap() []error method. When err wraps multiple
// errors, Is examines err followed by a depth-first traversal of its children.
//
// An error is considered to match a target if it is equal to that target or if
// it implements a method Is(error) bool such that Is(target) returns true.
//
// An error type might provide an Is method so it can be treated as equivalent
// to an existing error. For example, if MyError defines
//
//  func (m MyError) Is(target error) bool { return target == fs.ErrExist }
//
// then Is(MyError{}, fs.ErrExist) returns true. See [syscall.Errno.Is] for
// an example in the standard library. An Is method should only shallowly
// compare err and the target and not call [Unwrap] on either.

// 用于判断错误`err`的错误树中, 是否存在与目标错误`target`匹配的错误。
func Is(err, target error) bool {
  if target == nil { // 异常情况,如果 target 为 nil,则比较 err 指针是否等于 target 指针,即 判断是否 err 和 target 都是 nil
   return err == target
  }

  // 判断`target`的类型是否是可比较的。如果是可比较的类型,则会调用`is`函数进行比较,否则直接返回`false`。
  isComparable := reflectlite.TypeOf(target).Comparable()
  return is(err, target, isComparable)
}

/*
`is`函数是一个递归函数,用于在错误树中进行深度优先遍历。它首先检查`err`和`target`是否相等,如果相等且`target`是可比较的,则返回`true`。
总结起来,实现了一个递归的错误匹配算法,用于判断错误树中是否存在与目标错误匹配的错误。它会遍历错误树中的每个错误,比较错误与目标错误是否相等或是否实现了`Is(error) bool`方法,并在遍历过程中进行适当的展开
*/
func is(err, target error, targetComparable bool) bool {
  for {
   // 如果是可比较的,且指针相同,则为 true
   if targetComparable && err == target {
    return true
   }
   // 如果是可比较的,且 err 实现了 Is interface{} 则用 err.Is(target) 判断
   if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
    return true
   }
   // 根据`err`的具体类型进行不同的处理:
   switch x := err.(type) {
   // 如果`err`实现了`Unwrap() error`方法,则调用`x.Unwrap()`获取下一级错误,并将其赋值给`err`继续进行遍历。如果下一级错误为`nil`,则表示错误树遍历结束,返回`false`。
   case interface{ Unwrap() error }:
    err = x.Unwrap()
    if err == nil { // 递归结束条件
     return false
    }
   // 如果`err`实现了`Unwrap() []error`方法,则通过`x.Unwrap()`获取一个错误切片,并对切片中的每个错误进行递归调用`is`函数进行比较。如果其中有任何一个错误与`target`匹配,返回`true`,否则返回`false`。
   case interface{ Unwrap() []error }:
    for _, err := range x.Unwrap() {
     if is(err, target, targetComparable) { // is() 是递归函数
      return true
     }
    }
    return false
   default: // 如果`err`不属于上述两种类型,则表示错误树遍历结束,返回`false`。
    return false
   }
  }
}

2.3 errors.As()

作用:判断被包装过的error是否为指定类型

2.3.1 使用

package main

import (
    "errors"
    "fmt"
)

// 首先,定义了一个`TypicalErr`类型,并实现了`Error()`方法,使其满足`error`接口。在`main`函数中,创建了一系列通过`fmt.Errorf`函数包装的错误。
type TypicalErr struct {
    e string
}

func (t TypicalErr) Error() string {
    return t.e
}

func main() {
    err := TypicalErr{"TypicalErr"}
    err1 := fmt.Errorf("wrap err: %w", err)
    err2 := fmt.Errorf("wrap err1: %w", err1)

    var e TypicalErr

第二组操作中,使用`=`运算符比较`err2``err1`,由于它们是不同的实例,所以比较结果为`false`,打印的结果是"[use = ] err2 != err1"。然后,使用`errors.As`函数,将`err2``err1`进行比较,由于`err2`的类型是`*fmt.wrapError`,可以成功地将它转换为`err1`的类型,所以匹配成功,打印的结果是"[use as] err2 as err1"。接着,使用`errors.Is`函数,将`err2``err1`进行比较,由于它们的类型不同,所以比较结果为`false`,打印的结果是"[use is] err2 not is err1"。

第三组操作中,使用`=`运算符比较`err2``e`,由于它们是不同的实例,所以比较结果为`false`,打印的结果是"[use = ] err2 != e"。然后,使用`errors.As`函数,将`err2``e`进行比较,由于`err2`的类型是`*fmt.wrapError`,无法直接转换为`TypicalErr`类型,但它包装的错误类型是`TypicalErr`,所以匹配成功,打印的结果是"[use as] err2 as e"。接着,使用`errors.Is`函数,将`err2``e`进行比较,由于它们的类型不同,所以比较结果为`false`,打印的结果是"[use is] err2 not is e"。

总结起来,通过不同的方法进行错误比较和匹配,可以根据实际情况选择适合的方式来处理错误。
    if err1 == e {
       fmt.Println("[use = ] err1 == e")
    } else {
       // 由于它们是不同的实例,指针不同, 所以比较结果为`false`,打印的结果是"[use = ] err1 != e"。
       fmt.Println("[use = ] err1 != e")
    }
    if errors.As(err1, &e) {
       // 因为 err1 的错误链, 包含了 e, 所以 err1 解封装以后和 &e 的类型相同
       fmt.Println("[use as] err1 as e")
    } else {
       fmt.Println("[use as] err1 not as e")
    }
    if errors.Is(err1, e) {
       // 因为 err1 的错误链, 包含了 e, 所以 err1 就是 e
       fmt.Println("[use is] err1 is e")
    } else {
       fmt.Println("[use is] err1 not is e")
    }
    //if errors.Is(err1, &e) {
    // fmt.Println("[use is] err1 is &e")
    //} else {
    //     // 命中此分支, 因为比较值变量和指针变量, 并没有意义
    // fmt.Println("[use is] err1 not is &e")
    //}

    if err2 == err1 {
       fmt.Println("[use = ] err2 == err1")
    } else {
       // 由于它们是不同的实例,指针不同, 所以比较结果为`false`,打印的结果是"[use = ] err1 != e"。
       fmt.Println("[use = ] err2 != err1")
    }
    if errors.As(err2, &err1) {
       // 因为 err2 的错误链, 包含了 err1, 所以 err2 解封装以后和 &err1 的类型相同
       fmt.Println("[use as] err2 as err1")
    } else {
       fmt.Println("[use as] err2 not as err1")
    }
    if errors.Is(err2, err1) {
       // 因为 err2 的错误链, 包含了 err1, 所以 err2 就是 err1
       fmt.Println("[use is] err2 is err1")
    } else {
       fmt.Println("[use is] err2 not is err1")
    }

    if err2 == e {
       fmt.Println("[use = ] err2 == e")
    } else {
       // 由于它们是不同的实例,指针不同, 所以比较结果为`false`,打印的结果是"[use = ] err1 != e"。
       fmt.Println("[use = ] err2 != e")
    }
    if errors.As(err2, &e) {
       // 因为 err2 的错误链, 包含了 e, 所以 err2 解封装以后和 &e 的类型相同
       fmt.Println("[use as] err2 as e")
    } else {
       fmt.Println("[use as] err2 not as e")
    }
    if errors.Is(err2, e) {
       // 因为 err2 的错误链, 包含了 e, 所以 err2 就是 e
       fmt.Println("[use is] err2 is e")
    } else {
       fmt.Println("[use is] err2 not is e")
    }
}

// go run
[use = ] err1 != e
[use as] err1 as e
[use is] err1 is e
[use = ] err2 != err1
[use as] err2 as err1
[use is] err2 is err1
[use = ] err2 != e
[use as] err2 as e
[use is] err2 is e

2.3.2 实现

// As finds the first error in err's tree that matches target, and if one is found, sets
// target to that error value and returns true. Otherwise, it returns false.
//
// The tree consists of err itself, followed by the errors obtained by repeatedly
// calling its Unwrap() error or Unwrap() []error method. When err wraps multiple
// errors, As examines err followed by a depth-first traversal of its children.
//
// An error matches target if the error's concrete value is assignable to the value
// pointed to by target, or if the error has a method As(interface{}) bool such that
// As(target) returns true. In the latter case, the As method is responsible for
// setting target.
//
// An error type might provide an As method so it can be treated as if it were a
// different error type.
//
// As panics if target is not a non-nil pointer to either a type that implements
// error, or to any interface type.

/*
总结起来,`As`函数用于在错误树中查找与目标错误匹配的第一个错误,并将其设置为目标错误的值。它提供了一种灵活的错误匹配机制,可以根据错误类型提供的`As`方法实现自定义的匹配逻辑。
*/
func As(err error, target any) bool {
  if err == nil { // 异常处理, err 为 nil
   return false
  }
  if target == nil { // 异常处理, target 不能为 nil, 因为要对 target 取反射
   panic("errors: target cannot be nil")
  }
  // 通过反射获取`target`的值和类型,并进行一些类型检查,包括检查`target`是否为非空指针、是否为接口类型或实现了`error`接口。
  val := reflectlite.ValueOf(target)
  typ := val.Type()
  if typ.Kind() != reflectlite.Ptr || val.IsNil() { // target 必须为指针, 必须非 nil
   panic("errors: target must be a non-nil pointer")
  }
  targetType := typ.Elem() // target 指针对应的值, 必须实现了接口, 且必须实现了 error interface{} 接口
  if targetType.Kind() != reflectlite.Interface && !targetType.Implements(errorType) {
   panic("errors: *target must be interface or implement error")
  }
  
  // 然后,调用`as`函数进行实际的匹配过程。
  return as(err, target, val, targetType)
}


// `as`函数通过遍历错误树进行深度优先遍历,类似于`errors.Is`函数。
func as(err error, target any, targetVal reflectlite.Value, targetType reflectlite.Type) bool {
  for {
   // 首先检查当前错误`err`是否可以被赋值给`targetType`类型,如果可以,则将错误的值设置为`target`的值,并返回`true`。
   if reflectlite.TypeOf(err).AssignableTo(targetType) {
    targetVal.Elem().Set(reflectlite.ValueOf(err))
    return true
   }
   // 接着,它检查`err`是否实现了`As(interface{}) bool`方法,并调用`x.As(target)`来判断是否匹配。如果匹配成功,则返回`true`。
   if x, ok := err.(interface{ As(any) bool }); ok && x.As(target) {
    return true
   }
   
   // 如果上述条件都不满足,根据`err`的具体类型进行不同的处理:
   switch x := err.(type) {
   // 如果`err`实现了`Unwrap() error`方法,则调用`x.Unwrap()`获取下一级错误,并将其赋值给`err`继续进行遍历。如果下一级错误为`nil`,则表示错误树遍历结束,返回`false`。
   case interface{ Unwrap() error }:
    err = x.Unwrap()
    if err == nil {
     return false
    }
   // 如果`err`实现了`Unwrap() []error`方法,则通过`x.Unwrap()`获取一个错误切片,并对切片中的每个错误进行递归调用`as`函数进行匹配。如果其中有任何一个错误与`target`匹配,返回`true`,否则返回`false`。
   case interface{ Unwrap() []error }:
    for _, err := range x.Unwrap() {
     if err == nil {
      continue
     }
     if as(err, target, targetVal, targetType) {
      return true
     }
    }
    return false
   default: // 如果`err`不属于上述两种类型,则表示错误树遍历结束,返回`false`。
    return false
   }
  }
}

// 最后,定义了一个`errorType`变量,用于表示`error`接口的反射类型。
var errorType = reflectlite.TypeOf((*error)(nil)).Elem()
  • 7
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

呆呆的猫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值