因为日志库本身只负责格式化和输出,不参与请求生命周期管理;TraceID必须在请求进入时生成、透传并绑定到日志上下文,否则每次log.Info()都是孤立事件,跨goroutine或中间件调用链时极易丢失。

为什么 logrus 或 zap 默认不带 TraceID?
因为日志库本身只负责格式化和输出,不参与请求生命周期管理。TraceID 必须在请求进入时生成、透传、绑定到日志上下文,否则每次 log.Info() 都是孤立事件。常见错误是:在 handler 里临时生成一个 uuid.NewString() 然后塞进字段——这看似有 ID,但跨 goroutine(比如异步任务、数据库回调)或中间件调用链时就断了。
如何用 context.Context 持有并透传 TraceID
Go 的标准方式是把 TraceID 存进 context.Context,再通过中间件注入 HTTP 请求生命周期。关键不是“怎么生成”,而是“怎么确保所有日志都拿到它”:
- 入口中间件(如 Gin 的
gin.HandlerFunc)用req.Header.Get("X-Trace-ID")尝试读取上游传递的 ID;未提供则用uuid.NewString()生成新 ID - 用
context.WithValue(ctx, traceKey, traceID)把 ID 绑定到 context,并传给后续 handler - 所有日志调用必须基于该 context 获取 ID,例如封装一个
LogWithCtx(ctx context.Context, msg string)函数 - 避免直接用全局 logger,否则无法感知 context 变化
在 zap 中动态注入 TraceID 字段
zap 不支持运行时动态字段,但可以用 zap.AddSync + zap.WrapCore 实现每条日志自动携带当前 context 中的 TraceID:
func TraceIDCore(core zapcore.Core) zapcore.Core {
return zapcore.WrapCore(core, func(c zapcore.Core) zapcore.Core {
return &traceIDCore{Core: c}
})
}
type traceIDCore struct {
zapcore.Core
}
func (t *traceIDCore) Check(ent zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry {
if ce == nil {
return nil
}
// 从 context 中提取 TraceID(需配合 middleware 设置)
if traceID := getTraceIDFromContext(ent.Logger.Core().With([]zap.Field{}).Logger); traceID != "" {
ce.AddField(zap.String("trace_id", traceID))
}
return t.Core.Check(ent, ce)
}
更轻量的做法是:在每个 handler 内用 logger.With(zap.String("trace_id", getTraceID(ctx))) 获取子 logger,然后全程使用该子 logger——简单、可控、无副作用。
立即学习“go语言免费学习笔记(深入)”;
HTTP 跨服务调用时 TraceID 怎么透传?
单体应用内靠 context 传递足够,但调用下游 HTTP 服务时,必须把 TraceID 写入请求头,否则链路就断了:
- 始终使用
X-Trace-ID(业界通用名,比自定义名更利于排查) - 下游服务也必须有同名中间件读取并注入 context,形成闭环
- 如果用了 gRPC,对应 header 是
metadata.MD{"x-trace-id": []string{traceID}} - 注意:不要在日志里打印完整 TraceID 到公开接口响应体,存在敏感信息泄露风险
真正容易被忽略的是异步场景——比如 HTTP handler 启动了一个 go func(){...}(),这个 goroutine 拿不到父 context,必须显式传入 ctx 或用 context.WithoutCancel(parentCtx) 衍生子 context,否则 TraceID 会丢失。


















