应使用结构化日志库(如 zap)并传入 error 类型字段,配合 %w 包装保留错误链,开启 caller 和 stacktrace,避免字符串拼接或手动调用 err.Error()。

Go 的 log 包本身不处理错误,只是输出字符串
很多人以为 log.Println(err) 就算“记录了错误”,其实它只是把 err.Error() 转成字符串打印,丢失了原始 error 类型、堆栈、上下文等关键信息。真正要收集可排查的错误日志,必须让错误对象本身参与日志流程。
推荐做法是:用结构化日志库(如 zap 或 zerolog)替代原生 log,并统一用 error 类型字段记录错误。例如:
logger.Error("failed to open config file", zap.Error(err), zap.String("path", cfgPath))
这样日志系统才能识别该字段为错误类型,后续做告警、聚合、堆栈展开才有效。
用 fmt.Errorf + %w 保留错误链,别用 fmt.Sprintf 拼接
错误日志失去可追溯性,往往是因为在层层包装时用了字符串拼接,切断了错误链。比如:
立即学习“go语言免费学习笔记(深入)”;
- ❌ 错误写法:
err = fmt.Errorf("failed to parse JSON: %s", err.Error())——%w缺失,errors.Is/errors.As失效 - ✅ 正确写法:
err = fmt.Errorf("failed to parse JSON: %w", err)—— 保留原始错误,支持向下断言和堆栈追踪
尤其在中间件、handler、数据库调用等场景,每层都应使用 %w 包装,否则 log 或 zap 即使记录了最外层错误,也无法还原底层根本原因。
避免在日志里重复调用 err.Error(),交给日志库处理
常见反模式:logger.Info("operation failed", zap.String("error", err.Error()))。这不仅冗余,还可能因 err 是 nil 导致 panic(某些自定义 error 实现未防 nil),也丢失了错误类型语义。
正确方式是直接传 err 值(前提是日志库支持):
logger.Error("db query failed", zap.Error(err), zap.String("sql", query))
如果非要用原生 log 且必须带错误,至少用 fmt.Printf("%+v", err) 获取带堆栈的格式(需 github.com/pkg/errors 或 Go 1.13+ 的 errors 包配合),但依然不如结构化日志可靠。
生产环境必须加 caller 和 stacktrace 配置
没有调用位置和堆栈的错误日志等于废日志。以 zap 为例,初始化时务必开启:
-
zap.AddCaller()—— 记录file:line -
zap.AddStacktrace(zapcore.ErrorLevel)—— 在 Error 级别自动附加堆栈(注意:仅对zap.Error(err)生效,不是所有字段) - 避免用
zap.String("stack", debug.Stack())手动抓——性能差、易误触发、不精准
另外,log.SetFlags(log.Lshortfile | log.LstdFlags) 对原生 log 仅加文件行号,不带堆栈,排查深层错误基本没用。
错误日志的价值不在“有没有”,而在“能不能顺藤摸瓜定位到第 3 层 goroutine 里那个被忽略的 rows.Err()”。堆栈、调用链、结构化字段,缺一不可。










