errors.unwrap 比 == 更可靠,因 go 错误是值而非类型,跨模块时相同内容的 errors.new 错误地址不同导致 == 失效;errors.is/as 依赖 unwrap 链实现语义化判断。

跨模块错误传递时,为什么 errors.Unwrap 比直接比较 == 更可靠
Go 的错误是值,不是类型,不同包里用 errors.New 或 fmt.Errorf 创建的错误即使内容相同,== 也会返回 false。跨模块时,调用方无法预知底层错误的具体地址或构造方式,硬比较必然失效。
- 用
errors.Is(err, targetErr)判断是否为某类错误(内部调用Unwrap链) - 用
errors.As(err, &target)提取包装的底层错误结构体(如自定义错误类型) - 避免在业务层写
if err.Error() == "xxx"—— 字符串匹配脆弱,且无法处理多层包装 - 如果模块 A 返回
fmt.Errorf("db failed: %w", sql.ErrNoRows),模块 B 只需errors.Is(err, sql.ErrNoRows)即可识别,无需知道 A 的错误构造逻辑
如何设计可跨模块识别的自定义错误类型
要让下游模块能稳定识别并响应你的错误,关键不是“暴露错误变量”,而是“提供可导出的判断函数”或“导出带方法的错误类型”。直接导出 var ErrNotFound = errors.New("not found") 在跨模块时容易被重复定义或误判。
- 导出一个判断函数,例如:
func IsNotFound(err error) bool { return errors.Is(err, errNotFound) },其中errNotFound是包内非导出变量 - 若需携带上下文(如 ID、时间),定义结构体并实现
Error()和Unwrap()方法,确保能参与Is/As链 - 不要在错误消息里塞结构化字段(如
"id=123, code=404"),这迫使调用方做字符串解析 —— 应该用字段+方法暴露 - 模块间错误契约应通过接口约定,例如:
type Temporary interface{ Temporary() bool },比字符串更健壮
使用 fmt.Errorf("%w") 包装错误时,哪些情况会导致链断裂
%w 是跨模块错误传递的核心机制,但它的行为依赖严格条件:仅当格式字符串中**有且仅有一个**%w,且对应参数为非 nil error 类型时,才会建立包装链。任何偏差都会退化为普通字符串错误。
- 写成
fmt.Errorf("retry %d: %w", n, err)✅ 正常包装 - 写成
fmt.Errorf("retry %d: %v, %w", n, msg, err)❌ 多个动词,%w被忽略,返回纯字符串错误 - 写成
fmt.Errorf("%w", nil)❌ 参数为 nil,返回nil错误,而非包装空链 - 在日志中误用
log.Printf("err: %w", err)——log不支持%w,会 panic 或静默转为%v
模块边界处是否应该重新 wrap 错误?什么情况下该保留原始错误
不是所有错误都需要重 wrap。过度包装会让错误链冗长难读;完全不 wrap 又丢失当前层语义。关键看调用方是否需要区分“本层失败”和“下游失败”。
立即学习“go语言免费学习笔记(深入)”;
- 需要暴露本层意图时 wrap:例如 storage 模块调用 HTTP 客户端失败,应
fmt.Errorf("fetching user profile: %w", httpErr),让上层知道这是“获取用户资料”环节失败 - 不需要 wrap 的场景:本层只是透传,且无新上下文(如中间件做 auth 后直接
handler.ServeHTTP),此时直接返回下游错误更清晰 - 敏感信息必须清理:若下游错误含密码、token 等,不能直接
%w,应fmt.Errorf("internal error: %w", errors.New("something went wrong"))并单独记录原始错误 - 性能敏感路径(如高频 RPC handler)慎用多层
%w,每次包装都分配内存;可用errors.Join合并同类错误,但注意它不支持Is链式匹配
Is 不生效,其实只是上游忘了用 %w,或者下游用了 ==。










