zap.Error() 不能直接记录 error 类型字段,它仅调用 err.Error() 提取字符串,不保留堆栈、原始类型或 wrapped error 链;应改用 zap.NamedError()(v1.21+)以序列化完整错误结构。

zap.Error() 不能直接记录 error 类型的字段
你传 err 给 zap.Error() 看似合理,但实际它只接受实现了 error 接口的值——问题在于,zap.Error() 内部会调用 err.Error() 提取字符串,然后塞进 "error" 字段;它**不会自动展开堆栈、不保留原始类型、也不记录 wrapped error 链**。
常见错误现象:zap.Error(err) 打出来的日志只有 "error": "connection refused",没有文件行号、没有 cause、没有 fmt.Printf("%+v", err) 那种带栈的格式。
- 要用
zap.NamedError()替代(Zap v1.21+),它会把 error 的完整结构(含栈、wrapped errors)序列化为嵌套 JSON - 老版本 Zap(zap.String("error", fmt.Sprintf("%+v", err)),但注意这会丢失结构化能力——整个堆栈变成一个字符串字段
- 如果用了
github.com/pkg/errors或 Go 1.13+ 的errors.Unwrap(),别指望zap.Error()自动递归展开
如何让 zap 记录 error 的完整调用栈和 wrapped chain
Zap 本身不解析 error 栈,得靠外部库或手动构造。最稳妥的做法是:在记录前,用 github.com/mitchellh/go-homedir 这类无关,真正关键的是 github.com/pkg/errors 或标准库 fmt.Errorf("...: %w", err) + errors.Format()(Go 1.20+)。
- Go 1.20+ 推荐:
zap.String("error_stack", fmt.Sprintf("%+v", err))——%+v会触发Formatter接口,显示栈和 wrapped 链 - 兼容旧版:引入
github.com/pkg/errors,用pkgerrors.WithStack(err)包装后再传给zap.String("error_stack", pkgerrors.ToString(err, 1)) - 别把
fmt.Sprintf("%+v", err)当成字段名,它只是值;字段名建议用"error_detail"或"stack",避免和默认"error"冲突
zap.NamedError() 的字段名不是固定的 "error"
zap.NamedError("my_err", err) 生成的 JSON 字段名就是 "my_err",不是 "error"。这点容易被忽略,导致日志平台(比如 Loki、Datadog)无法自动识别错误字段。
立即学习“go语言免费学习笔记(深入)”;
- 如果你依赖日志系统做错误聚合,最好统一用
zap.Error(err)(仅字符串)+ 单独zap.String("error_detail", ...)(带栈)组合 -
zap.NamedError()适合多 error 场景,比如同时记录validation_err和db_err,但字段名要和监控规则对齐 - 注意
NamedError()在 Zap v1.21+ 才有,低版本会 panic;上线前务必检查go list -m go.uber.org/zap
自定义 error 字段时,避免 JSON 序列化失败
有些 error 实现里嵌了 sync.Mutex 或 http.Response 这类不可序列化字段,直接传给 zap.Any() 会导致 panic 或静默丢弃。
- 永远不要用
zap.Any("err", err)—— 它会尝试 JSON marshal 整个 error 对象,大概率崩 - 所有 error 相关字段,只用
zap.String()、zap.NamedError()或zap.Error() - 如果 error 里有敏感字段(如 token、password),记得在
fmt.Sprintf("%+v", err)前先清洗,Zap 不做脱敏










