Go中context.WithValue传不下去的根本原因是后续代码未使用其返回的新ctx,因Context不可变;HTTP需显式r.WithContext,goroutine需传newCtx,key须为私有struct,HTTP客户端需手动设header并传WithContext,gRPC metadata键名需小写,日志需用zap/zerolog从ctx提取trace_id。

Go 中 context.WithValue 为什么总传不下去
根本原因不是你没调用 context.WithValue,而是后续代码压根没用它返回的新 ctx。Go 的 context.Context 是不可变的,WithValue 返回的是新上下文,原 ctx 不会改变。
- HTTP handler 中必须显式写
r = r.WithContext(newCtx),否则下游r.Context()还是旧的 - Gin/Echo 中间件里,
next.ServeHTTP(w, r)传的是原始r,得改成next.ServeHTTP(w, r.WithContext(newCtx)) - goroutine 启动时,别写
go fn(r.Context()),要写go fn(newCtx),否则子协程拿不到值 - key 类型必须是私有 struct(如
type traceIDKey struct{}),不能用字符串,否则不同模块 key 冲突导致覆盖或取不到
HTTP 客户端透传 TraceID 时 header 总是空
原生 http.Client.Do 不读 context,也不会自动把 TraceID 写进 header——这一步必须手动做,且容易漏掉两个关键点。
- 先从
ctx.Value(traceIDKey{})取值,注意判断是否为nil,避免 panic - 构造请求后,用
req.Header.Set("X-Trace-ID", traceID),不要用Add,否则可能重复写入 - 发起请求时必须传带上下文的请求:
client.Do(req.WithContext(ctx)),否则超时/取消逻辑失效 - 若用
resty或gorequest等第三方 client,它们默认不处理 context,得手动.SetHeader("X-Trace-ID", traceID)
gRPC metadata 里取不到 X-Trace-ID
gRPC metadata key 会被自动转成小写,X-Trace-ID 发过去变成 x-trace-id,服务端用 md.Get("X-Trace-ID") 肯定为空。
- 客户端注入时用小写键名:
metadata.Pairs("x-trace-id", traceID) - 服务端提取时也用小写:
md.Get("x-trace-id") - 别只在拦截器里取一次就完事——要重新用
context.WithValue注入到 handler 的 ctx 中,否则业务层拿不到 - 如果用了
otelgrpc插件,它会自动处理 W3Ctraceparent格式,但前提是 client 和 server 都启用,且没手动覆盖 metadata
日志里始终没打出来 trace_id
log.Printf 或 fmt.Println 永远不会自动读 context,这是最常被忽略的一环。日志没 ID,不是 context 没传,是 logger 没“接住”。
立即学习“go语言免费学习笔记(深入)”;
- 别用标准库
log,换zap或zerolog:用zap.L().With(zap.String("trace_id", traceID))构建 logger 实例,存进 context - handler 里取 logger 要类型断言:
logger, ok := r.Context().Value(loggerKey{}).(*zap.Logger) - 如果用
zerolog,推荐zerolog.Ctx(r.Context()),它会自动从 context 提取trace_id字段(前提是之前存的是zerolog.TraceID或同名字段) - 注意 zap 的
With是浅拷贝,每次With都生成新 logger,别在循环里反复调用造成内存浪费
ctx 递过去。


















