Go 源码解读|如何用好 errors 库的 errors.Is() 与 errors.As() 方法

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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 = &wrapErrors, 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 接口。

for 循环判断 err 是否可以赋值给 target 所属类型,如果可以则赋值返回 true。

如果 err 实现了自己的 ​​As​​ 方法,则调用其逻辑,否则也是走递归拆包的逻辑。

小结

后续将继续分享一些源码解读的文章,关于 Go 语言的学习,我也开源了一个 GitHub 仓库,正在更新中,你也可以从我往期的文章中看到一些说明。

以上是关于Go 源码解读|如何用好 errors 库的 errors.Is() 与 errors.As() 方法的主要内容,如果未能解决你的问题,请参考以下文章

如何用go自定义prometheus的exporter

webpack 启动代码源码解读

Go语言系列之日志库zap

Go语言系列之日志库zap

双11,电商将如何用行动解读新零售?

如何用JS/HTML将时间戳转换为“xx天前”的形式附源码,转