Go标准库log包不支持链路追踪,因其无上下文感知能力;需通过context.WithValue注入traceID,并封装ContextLogger实现自动前缀输出,或改用zap/zerolog等原生支持context的第三方日志库。

为什么 log 包本身不支持链路追踪
Go 标准库的 log 包是线程安全但无上下文感知的,每次调用 log.Printf 都是独立事件,不会自动携带请求 ID、调用栈层级或父 span 信息。如果你在 HTTP handler 里直接打日志,所有日志行看起来都一样,根本分不清哪条属于哪个请求、哪个 goroutine、哪次重试。
真正需要的不是“加个中间件”,而是让日志输出能读取当前 goroutine 绑定的 trace 上下文——这必须靠 context.Context 传递,并由日志器主动提取。
用 context.WithValue 注入 traceID 最简可行方案
HTTP 中间件中生成唯一 traceID(比如用 uuid.NewString()),通过 context.WithValue 塞进 request context,后续所有日志调用都应基于该 context 提取并格式化输出。
- 不要用全局变量或 map 存 traceID:goroutine 不安全,且无法跨协程传播
- 避免在 handler 内部反复调用
ctx.Value("trace_id"):封装成辅助函数更可靠 - 务必在日志前缀里固定字段名,例如
[trace_id=xxx],方便 grep 和日志系统解析
示例中间件片段:
func TraceIDMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
traceID := uuid.NewString()
ctx := context.WithValue(r.Context(), "trace_id", traceID)
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}
自定义 log.Logger 实现 context 感知日志输出
标准 log.Logger 不接受 context,所以得包装一层:构造一个带 WithContext(context.Context) 方法的日志器,内部从 context 提取 traceID 并前置到每条日志。
- 不要继承
log.Logger:它没暴露底层 writer 和 flags,继承不可行 - 推荐组合方式:持有一个
*log.Logger实例,Printf时先拼接 traceID 前缀再透传 - 注意并发:
*log.Logger本身是 safe 的,但你的前缀拼接逻辑不能引入共享状态
关键代码示意:
type ContextLogger struct {
std *log.Logger
}
func (l *ContextLogger) Println(ctx context.Context, v ...interface{}) {
if id := ctx.Value("trace_id"); id != nil {
l.std.Println(fmt.Sprintf("[trace_id=%s]", id), v...)
} else {
l.std.Println(v...)
}
}
用 zap 或 zerolog 替代标准库更省心
如果项目允许引入第三方日志库,zap 和 zerolog 原生支持 context-aware 字段注入,比手撸兼容层稳定得多。它们的 With 方法可返回带字段的子 logger,配合中间件把 traceID 注入一次,后续所有日志自动携带。
-
zap推荐用zap.String("trace_id", id)+logger.With(...).Info(...) -
zerolog更轻量,req.Context().WithValue(...)后用log.With().Str("trace_id", id).Logger() - 注意:这些库默认不读 context,仍需中间件显式提取并传入,只是日志构造逻辑被封装好了
容易忽略的一点:HTTP 超时、panic 恢复、异步 goroutine(如 go func() {...}())中的日志,context 可能已失效或未传递——这些地方必须手动把 traceID 从原始 request context 复制过去,否则链路就断了。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











