Go 1.13 的 errors.Is 和 errors.As 通过递归调用 Unwrap() 判断错误链中是否存在目标错误或类型,但要求每层都用 %w 包裹且 Unwrap() 正确返回被包裹错误,否则链断裂导致判断失败。

Go 1.13 的 errors.Is 和 errors.As 怎么用才不踩坑
Go 1.13 引入的错误链(error wrapping)不是为了“套娃”,而是让错误判断更可靠。但很多人直接拿 errors.Is 去比对底层原始错误,却忘了中间可能被多次 fmt.Errorf("xxx: %w", err) 包裹 —— 这时它确实能穿透,但前提是每层都用了 %w,而不是 %v 或字符串拼接。
常见错误现象:errors.Is(err, io.EOF) 返回 false,明明最里层是 io.EOF;原因往往是某一层用了 fmt.Errorf("read failed: %v", err),断掉了错误链。
-
errors.Is只认实现了Unwrap() error方法的错误(比如fmt.Errorf("%w")构造的),且会逐层调用Unwrap()直到匹配或返回nil -
errors.As同理,但用于类型断言:它会遍历整个链,找到第一个能成功赋值给目标类型的错误实例 - 别在日志里盲目用
%+v打印错误链——它依赖fmt.Formatter实现,标准库只对fmt.Errorf链做了支持;自定义错误类型若没实现,%+v就只打当前层
Unwrap() 方法为什么不能随便返回 nil
一个错误类型只要实现了 Unwrap() error,就被视为可展开的错误节点。但返回 nil 不代表“没包裹”,而是告诉 errors.Is/errors.As:“到此为止,别往下查了”。这和“包裹了 nil 错误”是两回事。
使用场景:你写了一个包装器,内部错误可能尚未初始化(比如 lazy init),这时 Unwrap() 返回 nil 是合法的,但会导致上层判断失效 —— 比如 errors.Is(myErr, fs.ErrNotExist) 直接跳过你的类型,不会继续检查你内部字段。
立即学习“go语言免费学习笔记(深入)”;
- 如果错误确实包裹了另一个错误,
Unwrap()必须返回那个错误,哪怕它是nil(Go 允许error类型变量为nil) - 如果当前错误是终端错误(没有包裹任何其他错误),
Unwrap()应该返回nil,这是规范行为 - 返回非
nil错误但又不是真实包裹关系(比如返回硬编码的io.ErrUnexpectedEOF),会误导错误链遍历逻辑,导致Is判断出错
自定义错误类型怎么安全支持 %w 和 Unwrap
不是所有自定义错误都能直接用 %w。只有当你在构造时显式保存了被包裹错误,并在 Unwrap() 中原样返回它,才算真正接入错误链。否则 fmt.Errorf("xxx: %w", err) 只是在帮你做一次标准封装,你的类型本身仍是“黑盒”。
示例:下面这个类型看似支持 %w,实则断链:
type MyError struct {
msg string
// 缺少 wrapped error 字段!
}
func (e *MyError) Error() string { return e.msg }
func (e *MyError) Unwrap() error { return nil } // ❌ 断链
正确写法:
type MyError struct {
msg string
err error // 显式持有
}
func (e *MyError) Error() string { return e.msg }
func (e *MyError) Unwrap() error { return e.err } // ✅ 透出被包裹错误
- 字段名不重要,关键是
Unwrap()返回值必须是构造时传入的那个错误实例(或其衍生) - 如果错误需要携带额外上下文(如 traceID、重试次数),建议用结构体字段存,不要塞进
Error()字符串里 —— 否则Is/As无法感知 - 避免在
Unwrap()中做计算或条件判断(比如“只在 debug 模式下返回”),这会让错误行为不可预测
errors.Unwrap 函数和 Unwrap() 方法的区别
errors.Unwrap 是一个工具函数,它只做一件事:调用参数错误的 Unwrap() 方法。它不会递归,也不会跳过 nil;它只是“调一次”。而 errors.Is 和 errors.As 内部才是真正的递归遍历器。
容易被忽略的地方:有人以为 errors.Unwrap(err) 能拿到最底层错误,其实它最多只剥一层。想手动展开链?得自己循环调用:
for err != nil {
if errors.Is(err, io.EOF) { ... }
err = errors.Unwrap(err) // 注意:这里 err 可能变成 nil,也可能还是个 wrapper
}
-
errors.Unwrap对不实现Unwrap()的错误返回nil,不是原错误 - 它不处理
nil输入:传入nil会 panic(文档明确写了) - 日常开发中,99% 的情况应该直接用
errors.Is/errors.As,而不是手写展开逻辑 —— 它们已处理好边界(比如nil错误、循环引用检测)
Unwrap(),而是决定哪一层该包裹、哪一层该终止,以及让团队所有人一致用 %w 而不是 %v。










