不能直接用全局变量存TraceID,因为Go并发下会互相污染;必须用context.WithValue安全透传,确保每个goroutine、中间件和出站调用都严格携带并提取。

为什么不能直接用全局变量存 TraceID
全局变量或包级变量在 Go 的并发场景下会互相污染——HTTP 处理函数、goroutine、中间件之间共享同一份变量,context 是唯一被 Go 官方设计用来安全传递请求生命周期内元数据的机制。不用 context,TraceID 在 goroutine 分叉后就丢失,日志里看到的全是空或错乱 ID。
如何从入站请求提取并注入 TraceID 到 context
标准做法是检查请求头(如 X-Request-ID 或 traceparent),若不存在则生成新 ID。关键点在于:必须用 context.WithValue 绑定到传入的 ctx,而不是修改原 ctx(它不可变)。
- 不要用
context.Background()覆盖原始请求上下文,否则中间件链和超时控制全失效 - 推荐使用字符串常量做 key,避免拼写错误:
var TraceIDKey = struct{}{} - 提取逻辑建议封装成中间件,例如 Gin 中:
func TraceIDMiddleware() gin.HandlerFunc { return func(c *gin.Context) { traceID := c.GetHeader("X-Request-ID") if traceID == "" { traceID = uuid.New().String() } ctx := context.WithValue(c.Request.Context(), TraceIDKey, traceID) c.Request = c.Request.WithContext(ctx) c.Next() } }
日志库怎么读取 context 里的 TraceID
主流日志库(zap、logrus、zerolog)本身不自动读 context,必须手动提取并注入字段。常见错误是只在 handler 开头取一次,漏掉异步 goroutine 中的日志。
- 每次打日志前都应从当前 goroutine 的
ctx中取:if tid, ok := ctx.Value(TraceIDKey).(string); ok { logger = logger.With(zap.String("trace_id", tid)) } - 如果用了
zap,可封装一个带 context 感知的SugaredLogger辅助函数,避免重复判断 - 注意:goroutine 启动时必须显式传入带 TraceID 的
ctx,go fn()不会继承父 context
跨服务调用时如何透传 TraceID
HTTP 调用需把 TraceID 写入请求头;gRPC 需通过 metadata.MD 注入。漏掉任一跳,链路就断了。
- HTTP 客户端示例:
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil) if tid, ok := ctx.Value(TraceIDKey).(string); ok { req.Header.Set("X-Request-ID", tid) } - gRPC 客户端要包装
ctx:md := metadata.Pairs("x-request-id", tid) ctx = metadata.InjectOutgoing(ctx, md) - 别依赖服务端自动生成 ID——下游服务也得按同样逻辑提取,否则跨语言/跨框架链路无法对齐
context.TODO() 或忘了传 ctx,那条日志就永远游离在链路之外。


















