必须用%w包装且每层实现Unwrap()才能保证errors.Is(err, fs.ErrNotExist)返回true;若中间用%v/%s拼接或自定义错误未实现指针接收者Unwrap方法,错误链断裂导致匹配失败。

Go 框架里要让错误可追溯、可判断、不丢类型,必须用 %w + Unwrap() 构建链,而不是拼字符串或手动塞堆栈。
为什么 errors.Is(err, fs.ErrNotExist) 总是 false?
常见现象:你明明在底层调用了 os.ReadFile,它返回了 fs.ErrNotExist,但上层用 errors.Is(err, fs.ErrNotExist) 却返回 false。
- 根本原因:中间某层用了
fmt.Errorf("xxx: %v", err)或fmt.Errorf("xxx: %s", err.Error())—— 这会把原始 error 转成字符串,链彻底断裂 - 正确写法必须是
fmt.Errorf("xxx: %w", err),且%w只能出现在格式化字符串末尾 - 如果用了自定义 error 结构体但没实现
Unwrap()方法,errors.Is和errors.As也查不到下游错误
自定义 error 结构体必须实现 Unwrap() 才能进链
光有 Error() 不够,errors.As 和 errors.Is 依赖 Unwrap() 向下穿透。结构体字段命名无所谓,但方法签名和返回值必须严格匹配。
-
Unwrap()必须是指针接收者:func (e *MyError) Unwrap() error - 如果这个 error 不包装其他 error,直接返回
nil;如果包装了(比如有Cause error字段),就返回那个字段 - 别在
Unwrap()里做日志、加字段、调用fmt.Sprintf—— 它只负责“解包”,不是格式化入口 - 示例:
type ConfigError struct { Path string Cause error } func (e *ConfigError) Error() string { return "failed to load config " + e.Path } func (e *ConfigError) Unwrap() error { return e.Cause }
HTTP handler 边界处要不要用 %w?
要看你是否需要向上游暴露底层错误语义。框架入口(如 ServeHTTP)是错误链的终点,也是敏感信息过滤点。
立即学习“go语言免费学习笔记(深入)”;
- 需要做类型判断(比如重定向、重试、降级)→ 用
%w,让errors.Is能穿透到io.EOF、net.OpError等原生错误 - 要隐藏细节(如数据库地址、内部服务名)→ 改用
%v或构造新 error,但务必单独记录原始 error 到日志(用log.Printf("raw err: %+v", err)) - 别在 handler 里写一长串
if errors.As(err, &e1) { ... } else if errors.As(err, &e2) { ... }—— 容易漏,建议统一映射到 HTTP 状态码表
errors.As(err, &target) 失败的三个高频原因
不是 target 类型写错了,而是链本身断了或类型不匹配。调试时优先检查这三点:
- 中间某层用了
%v或%s包装,导致原始 error 被转成字符串 → 链断,Unwrap()返回 nil - 自定义 error 是值接收者实现
Unwrap()(如func (e MyError) Unwrap()),导致指针比较失败 → 必须用指针接收者 - 不同包定义了同名结构体(比如
user.NotFoundError和auth.NotFoundError),哪怕字段完全一样,Go 也认为是不同类型 →errors.As不会跨包匹配
真正难的不是写 Unwrap(),而是守住每一层的包装规范:谁该用 %w、谁该截断、谁该记录原始 error —— 这些决策点分散在各业务函数里,一旦松动,整条链就失效。


















