Go 重难点拆解|errors.Is、errors.As 与 errors.AsType 的分工
Go 1.13 引入了错误包装:用 fmt.Errorf("...: %w", err) 给底层错误加上下文,包装后的错误通过 Unwrap() 方法还能取回原来的错误。这个机制带来一个直接后果:错误一旦被包装,最外层就不再是原来的类型和值,err == ErrNotFound 和 err.(*MyErr) 这两种最常见的判断方式都会失效。
errors 包为此提供了三个函数:errors.Is、errors.As,以及 Go 1.26 新增的 errors.AsType。这篇文章按几个具体问题展开,说清它们各自解决什么问题、内部如何工作、常见的误用在哪里。
一、errors.Is 解决什么问题
问题:我定义了一个哨兵错误(sentinel error)ErrNotFound,下层返回它,中间层用 fmt.Errorf("query user: %w", err) 包了一层,上层用 err == ErrNotFound 判断,为什么永远是 false?
因为 == 比较的是接口值的动态类型和动态值。包装之后,err 的动态类型是 *fmt.wrapError,动态值是那个包装结构体,和 ErrNotFound(动态类型 *errors.errorString)不可能相等。原始错误还在,只是被放进了 Unwrap() 里,== 看不到。
这种情况应该用 errors.Is 来判断。先看它要交付什么:这个场景只需要知道”链里有没有 ErrNotFound 这个值”,一个 bool 就够了,不需要把任何东西交还给调用方。errors.Is 就是”沿着包装链逐层比较”的 ==:
1 | var ErrNotFound = errors.New("not found") |
errors.Is(err, target) 从 err 开始,每一层做两件事:先判断这一层是否与 target 相等(值可比较时用 ==,或者这一层实现了 Is(error) bool 方法且返回 true),不相等就调用 Unwrap() 取下一层,直到匹配或链到底。
两个补充:
- 自定义错误类型可以实现
Is(target error) bool方法,让errors.Is按你定义的规则判定相等。典型用法是带字段的错误值和一个不带字段的哨兵之间做”类别相等”。 - Go 1.20 起
errors.Join和%w多参数会产生Unwrap() []error的树状结构,errors.Is会深度优先遍历整棵树,不只是单链。
因为 errors.Is 只交付一个 bool,它的第二个参数是普通的 error 值,不需要取地址。记住”交付一样东西”这个说法,下面 errors.As 要交付两样东西,设计上的差别都来自这里。
二、errors.As 解决什么问题
问题:我定义了一个带字段的错误类型 *NetworkError,想在上层读它的 Op、URL 字段。但 err 的静态类型是 error,接口上只有 Error() 方法,编译器不允许 err.Op。类型断言 err.(*NetworkError) 可以拿到字段,但它只看最外层,被包装后就失效了。怎么办?
和 errors.Is 不同,这个场景需要交付两样东西:
- 链里有没有
*NetworkError这个类型,用 bool 回答; - 有的话,那个具体的
*NetworkError值是什么,交给调用方读字段。
只给第 1 样不够:你知道链里”有”,但手里没有静态类型为 *NetworkError 的变量,Op、URL 照样读不了。errors.As 就是”沿着包装链逐层做类型断言”,并且把断言成功的那个值交给你:
1 | type NetworkError struct { |
签名是 func As(err error, target any) bool。第一个参数 err 是被搜索的错误链;第 1 样东西通过返回值 bool 交付;第 2 样东西写进第二个参数 target 所指向的变量,也就是例子里的 netErr。
三、errors.As 的第二个参数为什么要传 &netErr
这是初学时最容易绕晕的地方,拆成四个小问题。
问题 1:errors.As 为什么要往第二个参数里写值?
第二节说过,errors.As 要交付两样东西,返回值 bool 只能交付第一样。第二样,也就是找到的那个 *NetworkError 值,必须另有出口,这个出口就是第二个参数:errors.As 找到匹配项后执行 *target = 找到的错误,把值写进 netErr。第二个参数存在的意义,就是接收这第二样东西。
问题 2:既然要写值,为什么必须传地址?
因为 Go 是值传递。如果写成 errors.As(err, netErr),函数拿到的是 netErr 的一份拷贝(一个 nil 指针),它无论怎么赋值,main 里的 netErr 依然是 nil,后面 netErr.Op 直接空指针 panic。只有传 &netErr,函数拿到变量的地址,才能把结果写回 main 里的那个变量。json.Unmarshal(data, &v) 是同一个模式:任何”函数帮你填变量”的 API,在 Go 里都只能通过传地址实现。
问题 3:netErr 本身已经是指针了,为什么还要再取一次地址?
netErr 是指针,和”传地址”是两件独立的事。netErr 的类型是 *NetworkError,是因为 Error() 方法是指针接收者,实现 error 接口的类型是 *NetworkError,错误链里装的动态类型也是 *NetworkError,接收变量的类型必须和它一致。而 &netErr(类型 **NetworkError)是为了让 errors.As 能修改 netErr 这个变量本身。如果 Error() 是值接收者、链里装的是 NetworkError 值,那接收变量就声明成 var netErr NetworkError,传的仍然是 &netErr,只是类型变成 *NetworkError。取地址这一步不会因为接收变量是不是指针而改变。
问题 4:Unwrap 作用在哪个参数上?
作用在第一个参数 err 上,而且是只读遍历:errors.As 内部用局部变量沿着 err.Unwrap() 一层层走,main 里的 err 不会被改动,所以第一个参数按值传就够了。第二个参数 netErr 不参与解包,它是唯一被赋值的变量。”被遍历”和”被修改”是两件事,前者发生在第一个参数上,后者发生在第二个参数上。
回头看,errors.As 用起来确实繁琐:调用方要先声明一个变量,再把它的地址传进去,函数内部通过反射看这个地址指向什么类型,再把找到的值填进去。三步里有两步是调用方在替函数做准备工作。原因在于 errors.As 是 Go 1.13 加入的,当时没有泛型,一个库函数无法返回”由调用方指定的任意类型”,只能用这种输出参数加反射的方式绕过去。这个绕法带来两个代价:反射意味着类型检查推迟到运行时,也就是第四节的两个陷阱;繁琐的调用形式则要等到泛型出现才有替代方案,也就是第五节的 errors.AsType。
四、errors.As 的两个运行时陷阱
errors.As 依赖反射,编译器不检查 target 的类型是否合理,下面两个要求只在运行时检查,不满足就 panic:
target必须是非 nil 指针。传netErr而不是&netErr,panic 信息为errors: target must be a non-nil pointer。target指向的类型必须实现error或者是接口类型。下面这种写法能编译通过,运行时 panic:
1 | var ne NetworkError // 值类型;Error 方法是指针接收者,NetworkError 本身不实现 error |
五、Go 1.26 的 errors.AsType[E]
errors.AsType 交付的仍然是第二节说的两样东西,区别在交付方式:有了泛型,”返回调用方指定的类型”不再需要输出参数,两样东西用两个返回值 (E, bool) 一次交付。Go 1.26 新增:
1 | func AsType[E error](err error) (E, bool) |
用法:
1 | if netErr, ok := errors.AsType[*NetworkError](err); ok { |
与 errors.As 对照:
errors.As |
errors.AsType[E] |
|
|---|---|---|
| 引入版本 | Go 1.13 | Go 1.26 |
| 目标类型如何指定 | 通过 target 指针的指向类型,运行时反射推断 |
类型参数 E,编译期确定 |
| 结果如何交付 | 写入 *target |
作为第一个返回值返回 |
| 需要预先声明变量 | 是,且通常在 if 之外 |
否,可以写在 if 的初始化语句里 |
| 类型错误何时暴露 | 运行时 panic | 编译期报错(E 不满足 error 约束时) |
| 实现方式 | 反射 | 类型断言 err.(E),无反射,无额外内存分配 |
两点需要注意:
E受error约束。errors.As允许target指向任意接口类型,例如var t interface{ Timeout() bool };errors.AsType要求E本身实现error,等价写法要改成errors.AsType[interface{ error; Timeout() bool }](err)。AsType[NetworkError](值类型)与AsType[*NetworkError](指针类型)是两个不同的类型参数。如果Error()是指针接收者,AsType[NetworkError]直接编译不过,这正是它比errors.As安全的地方;如果Error()是值接收者,两者都能编译,但链里装的是*NetworkError时,AsType[NetworkError]返回 false,需要按实际装入的类型选择。
errors.As 没有被标记为废弃,标准库文档的表述是”多数场景优先使用 AsType“。项目 Go 版本在 1.26 及以上的新代码,建议直接用 errors.AsType;需要兼容旧版本的代码继续用 errors.As。
六、三者如何选
判断依据是”你要从错误链里拿回什么”:
- 只需要知道链里是否存在某个特定的错误值(哨兵),用
errors.Is。 - 需要拿到某个类型的错误值并读它的字段或调它的方法,Go 1.26 以上用
errors.AsType[E],更早版本用errors.As。 - 比较的是哨兵错误,且确定这个错误没有经过任何包装(例如自己包内直接返回、直接判断),
err == ErrNotFound即可,不必引入errors包。
小结
errors.Is 是能穿透包装的 ==,errors.As 是能穿透包装的类型断言。errors.As 的第二个参数传地址,是因为它除了返回”有没有”之外,还要把找到的具体值交给调用方,而 Go 值传递下只有传地址才能写回调用方的变量;Unwrap 作用在第一个参数上且是只读遍历,与第二个参数无关。errors.AsType[E] 用泛型把”输出参数”改回了”返回值”,去掉了反射和运行时类型检查,是 Go 1.26 之后的推荐写法。