errors.is 找不到包装错误是因为只检查错误链中是否存在目标错误值,不比较类型或消息;若用 %v 而非 %w 包装,错误链断裂,导致匹配失败。

为什么 errors.Is 找不到你包装过的错误
因为 errors.Is 只检查「错误链中是否存在某个目标错误值」,它不比较类型、不反射、不看消息,只认 == 或实现了 Is(error) 方法的错误。如果你用 fmt.Errorf("wrap: %w", err) 包装,原始错误还在链里;但若用了 fmt.Errorf("wrap: %v", err)(漏了 %w),整条链就断了——后续 errors.Is 必然失败。
常见错误现象:errors.Is(err, io.EOF) 返回 false,即使你确定上游返回了 io.EOF。
- 确认所有包装都用
%w,不是%v或%s - 避免在中间层把错误转成字符串再重建(比如
errors.New(err.Error())),这会彻底丢弃原错误和包装关系 -
os.PathError、net.OpError等标准错误自带Unwrap(),可被errors.Is正常遍历;自定义错误记得实现Unwrap() error
errors.As 总是返回 false?检查你的目标变量类型
errors.As 的作用是「把错误链里的某个具体类型错误提取出来」,但它要求传入的是指针,且该指针指向的类型必须能接收链中某一级的错误实例。最常见错误:传了值类型或指针类型不匹配。
使用场景:你想拿到底层的 *os.PathError 来读取 Path 字段,或者捕获自定义的 *MyAppError 并访问其 Code 字段。
立即学习“go语言免费学习笔记(深入)”;
- 必须传二级指针:
var perr *os.PathError; errors.As(err, &perr),不是errors.As(err, perr) - 目标类型不能是接口(如
error)、不能是未导出字段的结构体(Go 1.20+ 对未导出字段限制更严) - 如果错误链里是
*os.PathError,但你声明的是os.PathError(值类型),errors.As会失败——必须用指针类型匹配
err := fmt.Errorf("read failed: %w", &os.PathError{Op: "open", Path: "/tmp/x", Err: syscall.ENOENT})
var perr *os.PathError
if errors.As(err, &perr) {
fmt.Println(perr.Path) // 输出 "/tmp/x"
}
什么时候不该用 %w,而该用 %v 或丢弃原错误
不是所有错误都需要保留原始上下文。过度包装会让错误链冗长、日志难读、调试时分不清主次。关键是看「下游是否需要根据原始错误做决策」。
性能影响:每次 %w 都会新增一层 Unwrap() 调用,errors.Is 和 errors.As 需要递归遍历;10 层以上错误链在高并发场景下有微小但可测的开销。
- 对外暴露的 API 错误(如 HTTP handler),建议用
%v或重写错误消息,避免泄露内部实现细节和敏感路径 - 已明确分类的业务错误(如
ErrNotFound),不需要再包装底层sql.ErrNoRows,直接返回即可 - 日志记录前,可以用
errors.Unwrap(err)获取最内层错误来判断是否忽略(如测试中的context.Canceled)
自定义错误怎么支持 errors.Is 和 errors.As
只要实现 Unwrap() error,就能被标准库错误函数识别。如果还想让 errors.Is 支持直接比对自身(比如 errors.Is(err, ErrTimeout)),就得额外实现 Is(error) bool 方法。
容易踩的坑:实现 Is 时忘了调用 errors.Is 检查嵌套错误,导致只匹配最外层。
- 必须提供
Unwrap()方法返回被包装的错误(可以是nil) - 如果希望
errors.Is(err, MyErr)成立,MyErr必须是变量(不是新构造的&MyError{...}),且Is方法里要包含errors.Is(wrapped, target) - 不要在
Is或Unwrap里 panic 或做 I/O,它们可能被日志、监控等基础设施频繁调用
type MyError struct {
Msg string
Code int
err error // 被包装的原始错误
}
func (e *MyError) Error() string { return e.Msg }
func (e *MyError) Unwrap() error { return e.err }
func (e *MyError) Is(target error) bool {
if e == target { return true }
return errors.Is(e.err, target) // 关键:递归检查
}
错误包装不是加得越多越好,关键在「谁消费这个错误」——调用方真需要知道是 syscall.ECONNREFUSED 还是只需要知道「服务不可用」,决定了你该断链还是留链。










