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() 方法的主要内容,如果未能解决你的问题,请参考以下文章