%w用于错误包装以保留原始错误引用,支持errors.Is、errors.As和errors.Unwrap;%v/%s仅转字符串导致类型与上下文丢失,且%w要求参数为error、每调用限一次、需防nil和循环包装。
它不是简单拼接字符串,而是让新错误“记住”旧错误是谁。这样上层代码就能用 errors.Is 判断是否源自 os.ErrNotExist,或用 errors.As 提取底层 *os.PathError,而不会因为加了一层描述就丢失关键信息。
%w 而不是 %v 或 %s用 %v 或 %s 只会调用原始错误的 Error() 方法,转成纯字符串——原始错误类型、堆栈、字段全丢光了。而 %w 是 Go 错误包装机制的“开关”,只有它才能触发 errors.Unwrap() 返回下一层错误。
%w 要求参数必须是 error 类型,传非 error 会 panicfmt.Errorf 调用里只能出现一次 %w,多写会编译失败fmt.Errorf(...%w...)),%w 会继续链下去,形成多层错误链典型误用:在日志或调试时顺手把错误当字符串塞进去,结果后续无法判断错误类型。
err := os.Open("config.yaml")
if err != nil {
// ❌ 错误:%v 消灭了错误类型
return fmt.Errorf("加载配置失败:%v", err)
// ✅ 正确:用 %w 保留可判断性
return fmt.Errorf("加载配置失败:%w", err)
}
调用方可以安全地做这些事:
errors.Is(err, os.ErrNotExist) → 返回 true(如果底层确实是文件不存在)var pathErr *os.PathError; errors.As(err, &pathErr) → 成功提取路径和操作信息errors.Unwrap(err) → 得到原始 *os.PathError
错误链不是越长越好。常见问题包括:
Unwrap()
%w,比如只写 fmt.Errorf("处理超时:%s", err.Error()),整条链就此断裂%w(fmt.Errorf("xxx: %w", nil))会返回 nil,而不是带上下文的新错误——这很隐蔽,建议加空值检查真正关键的不是“加了多少层”,而是“每一层是否提供了不可替代的上下文”,以及“最底层错误是否始终可触达”。