fmt.errorf用%w包装错误时,底层将原错误存入新错误的unwrap()方法中,使errors.is/as可穿透;若用%v或%s则仅拼字符串,unwrap()返回nil,错误链断裂。

fmt.Errorf 用 %w 包装错误时,底层到底发生了什么
它不是简单拼字符串,而是把原错误存进新错误的 Unwrap() 方法里,让调用方能用 errors.Is() 或 errors.As() 向下穿透。没用 %w 就只是普通字符串错误,Unwrap() 返回 nil。
常见错误现象:用了 errors.Is(err, io.EOF) 却返回 false,其实是因为上层错误是用 %s 拼出来的,根本没保留原始错误链。
-
%w只接受一个error类型参数,多传或类型不对会 panic - 不能写成
fmt.Errorf("read failed: %w", err1, err2)—— 这会编译失败 - 如果想包装多个错误,得自己实现
Unwrap() error或用第三方包(如multierr)
什么时候必须用 %w,而不是 %v 或 %s
当你希望下游能做错误判断、重试控制或日志分级时,比如数据库操作失败后要区分是连接断开还是 SQL 语法错,就得靠 %w 把原始错误透传下去。
使用场景举例:HTTP handler 中封装 DB 错误,中间件需要根据 errors.Is(err, sql.ErrNoRows) 返回 404,而不是 500。
- 用
%v:适合调试日志,只看信息,不参与错误处理逻辑 - 用
%s:适合生成用户可见提示,比如"文件不存在:" + err.Error() - 用
%w:仅当你要保留错误可判定性,且下游确实会调用errors.Is或errors.As
嵌套包装时 %w 的行为和陷阱
Go 的错误包装是单向链表,%w 只保留一个直接包裹的错误。连续两次用 %w 不会形成“双层包装”,第二次会覆盖第一次。
示例:
err := fmt.Errorf("layer1: %w", io.EOF)
err = fmt.Errorf("layer2: %w", err)此时 errors.Unwrap(err) 返回的是那个 fmt.Errorf("layer1: %w", io.EOF) 实例,再 Unwrap() 一次才到 io.EOF。
- 不要在循环中反复用
%w包装同一错误——只会延长链,不增加信息 - 如果中间某层改用
%v,链就断了:fmt.Errorf("broken: %v", wrappedErr)→Unwrap()返回nil - 注意性能:频繁包装深层错误可能影响堆栈可读性,但不会显著拖慢
errors.Is
如何验证 %w 是否生效
别光看 err.Error() 输出,得实测 Unwrap() 和 Is()。
最简验证方式:
original := io.EOF
wrapped := fmt.Errorf("read failed: %w", original)
fmt.Println(errors.Is(wrapped, io.EOF)) // true
fmt.Println(errors.Unwrap(wrapped) == original) // true
- 测试时别用
fmt.Sprintf替代fmt.Errorf—— 它不支持%w - 如果用
log.Printf("%+v", err)查看,有些 logger(如log/slog)会自动展开错误链,但标准fmt不会 - 第三方错误库(如
github.com/pkg/errors)的Wrap行为类似,但接口不兼容 Go 原生errors包
真正容易被忽略的是:包装本身不记录时间或 goroutine ID,如果需要上下文追踪,得配合 fmt.Errorf("at %s: %w", time.Now(), err) 或用结构化日志加字段,而不是指望 %w 自动带出额外信息。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











