log.Printf 无法打印错误堆栈是因为标准库 log 不捕获 goroutine 堆栈,%v 仅输出错误文本,需用 %+v 配合支持堆栈的错误类型或改用 zap.Error 等方案。

为什么 log.Printf 无法打印错误堆栈
Go 标准库的 log 包默认不捕获 goroutine 堆栈,log.Printf("%v", err) 只输出错误值本身(比如 "connection refused"),没有调用链。如果错误来自深层函数、或被多层包装(如 fmt.Errorf("failed to read: %w", err)),仅靠 %v 会丢失上下文。
解决方法是用 %+v 配合支持堆栈的错误类型(如 github.com/pkg/errors 或 Go 1.13+ 的 errors.Unwrap 衍生类型),但更直接的是改用 log.Print + debug.PrintStack() 手动追加,或切换到支持结构化日志的库。
- 标准
log不自动包含文件名、行号、时间戳 —— 需手动传入log.Lshortfile | log.LstdFlags -
log.Fatal会直接os.Exit(1),不适合在 HTTP handler 等需继续响应的场景使用 - 并发写日志时,
log是线程安全的,但若重定向到同一文件需额外加锁或使用带轮转的 logger
用 zap 记录带堆栈和字段的错误日志
zap 是生产环境最常用的高性能结构化日志库,能天然记录错误堆栈、调用位置,并支持字段扩展。关键不是“怎么打日志”,而是“怎么把错误信息完整塞进去”。
推荐用 zap.Error(err) 而非 zap.String("err", err.Error()):前者会自动调用 err.Error(),并在启用 development 模式时展开堆栈;若错误实现了 StackTrace() errors.StackTrace(如 github.com/pkg/errors 返回的 error),zap.Error 还会序列化堆栈帧。
立即学习“go语言免费学习笔记(深入)”;
- 开发环境用
zap.NewDevelopment(),自动带颜色、文件行号、完整堆栈 - 生产环境用
zap.NewProduction(),JSON 输出、无堆栈(可手动开启:zap.AddStacktrace(zapcore.ErrorLevel)) - 不要对
err做二次fmt.Sprintf再传给zap.Error,这会破坏原始错误类型和堆栈 - 示例:
logger.Error("failed to process request", zap.String("path", r.URL.Path), zap.Error(err))
自定义 error 类型 + fmt.Formatter 实现统一日志格式
当团队有统一错误码、业务上下文(如请求 ID、用户 ID)要求时,标准 error 接口太单薄。可以实现 fmt.Formatter 接口,让 log.Printf("%+v", myErr) 自动输出结构化内容。
例如定义 AppError 类型,内嵌 error,并实现 Format 方法:遇到 verb == '+' 时,输出错误消息、码、堆栈(用 debug.Stack() 截取)、以及附加字段。这样所有 logger(包括标准 log、zerolog)只要用 %+v 就能一致渲染。
- 避免在
Format中调用debug.PrintStack()(它直接写 stderr),应改用debug.Stack()获取字节切片再截断 - 堆栈过长会拖慢日志性能,建议限制最多 20 行:
bytes.SplitN(stack, []byte("\n"), 21)[0] - 字段如
reqID应通过构造函数注入,而非从全局 context 取 —— 否则并发日志会串扰
HTTP handler 中记录错误日志的常见陷阱
在 http.HandlerFunc 里,错误常来自解析参数、调用下游、DB 查询等。容易犯的错是:只在顶层 if err != nil 记一次日志,却没区分“客户端错误”(4xx)和“服务端错误”(5xx),导致告警无法精准触发。
- 对
net/http的url.Error、io.EOF、context.Canceled等要分类处理:它们通常不记 ERROR 级别,而是 DEBUG 或 INFO - 不要在中间件里用
recover()+log.Fatal—— 这会让整个 server crash,应log.Error后返回 500 响应 - 记录日志时务必带上
zap.String("method", r.Method)、zap.String("path", r.URL.Path)、zap.String("user_id", userID),否则排查时无法关联请求 - 敏感字段(如密码、token)禁止进日志:检查
err.Error()是否含敏感字符串,或统一用zap.String("err_type", reflect.TypeOf(err).String())替代原始消息
真正难的不是选哪个库,而是决定哪些错误值得记堆栈、哪些只需记类型、哪些该丢弃 —— 这取决于你的监控粒度和存储成本。堆栈不是越多越好,而是要在可定位性和性能损耗之间卡住那个点。










