errors.Is 不能直接比对 fmt.Errorf("xxx"),因它只匹配错误变量(如 io.EOF、包级 ErrTimeout),不匹配动态构造的错误实例;错误链需每层用 %w 且位于末尾才能穿透。

errors.Is 为什么不能直接比对 fmt.Errorf("xxx")
因为 errors.Is 只匹配错误值(如 io.EOF、os.ErrNotExist 或你定义的 var ErrTimeout = errors.New("timeout")),不匹配运行时动态构造的错误实例。写成 errors.Is(err, fmt.Errorf("not found")) 永远返回 false,哪怕错误信息一模一样。
真正起作用的是错误链中某一层是否等于目标变量——所以必须用变量或标准库导出的错误常量:
-
errors.Is(err, io.EOF)✅ 正确,io.EOF是导出变量 -
errors.Is(err, ErrTimeout)✅ 正确,前提是ErrTimeout是包级变量,不是函数内errors.New的临时值 -
errors.Is(err, nil)❌ 永远是false;判断空错误仍得用err == nil
errors.As 传参必须是指针,且类型要具体
errors.As 不是类型断言的语法糖,它会遍历错误链,尝试把第一个能赋值给目标类型的错误实例写入你提供的变量。这要求你传入的是指向该类型的指针,且类型必须具体(不能是接口,除非该接口自己实现了 As(interface{}) bool)。
常见写法:
-
var pathErr *os.PathError; if errors.As(err, &pathErr) { ... }✅ 提取底层*os.PathError -
var target MyCustomError; errors.As(err, &target)✅ 前提是MyCustomError是结构体类型,且你传的是它的地址 -
errors.As(err, pathErr)❌ 编译失败:不能把*os.PathError当作interface{}传进去 -
errors.As(err, &err)❌ 无意义,err是error接口类型,不满足“具体类型”要求
错误链断裂:%w 用错位置或中间混入 %v 就失效
errors.Is 和 errors.As 能穿透多层包装,前提是每层都用 fmt.Errorf("msg: %w", err),且 %w 是唯一、末尾的动词。任何偏离都会导致错误链在那一层中断。
典型断裂场景:
-
fmt.Errorf("read failed: %v", err)❌%v不触发包装,Unwrap()返回nil,链在此截断 -
fmt.Errorf("read failed: %w, retry=%d", err, n)❌%w不在末尾,编译报错 -
fmt.Errorf("read failed: %w", errors.New("bad"))❌errors.New没有Unwrap()方法,下一层无法继续展开 -
fmt.Errorf("read failed: %w", someNilError)✅ 允许,someNilError是error类型变量且值为nil,Unwrap()可返回nil,符合规范
自定义错误类型必须实现 Unwrap、Is、As 才能参与链式判断
如果你写了包装型错误(比如 type MyWrap struct{ inner error }),只实现 Error() 是不够的。要让 errors.Is 和 errors.As 能正确识别和提取它,必须显式提供:
-
func (e MyWrap) Unwrap() error { return e.inner }—— 否则errors.Is根本不会往下查 -
func (e MyWrap) Is(target error) bool { return errors.Is(e.inner, target) }—— 否则errors.Is(err, MyWrap{})永远不成立 -
func (e MyWrap) As(target any) bool { return errors.As(e.inner, target) }—— 否则errors.As无法从你的类型里继续提取更深层错误
最容易被忽略的一点:如果 inner 是 nil,Unwrap() 仍应返回 nil,而不是 panic 或跳过——这是错误链终止的合法信号,不是 bug。


















