context.WithValue根本不能跨服务传Span,因Context仅进程内有效且不序列化;跨服务必须用traceparent头或gRPC metadata.MD透传,并通过OTel propagator.Inject/Extract显式控制提取、注入、传输、解析四步。

context.WithValue根本不能跨服务传Span
直接用 context.WithValue 设置的 Span 或 traceID,在 HTTP/gRPC 发出去后,下游服务完全收不到。Go 的 context.Context 是进程内内存结构,不参与序列化和网络传输——它连 JSON 都不会自动转,更别说跨机器了。
常见错误是:上游塞了 context.WithValue(ctx, traceKey{}, "abc123"),下游一调 ctx.Value(traceKey{}) 就返回 nil,链路直接断裂。
- 跨服务唯一可靠载体是 HTTP Header(如
traceparent)或 gRPC 的metadata.MD -
context.WithValue只负责「本地存」和「本地取」,中间的「提取→注入→传输→解析」四步必须你手动或 SDK 显式控制 - OpenTelemetry 默认用 W3C TraceContext 标准,写的是
traceparent: 00-abc123-def456-01,不是X-Trace-ID这种自定义头(除非你配了自定义 propagator)
HTTP 客户端必须调用 propagator.Inject()
不是简单地 req.Header.Set("traceparent", ...),而是要用 OpenTelemetry 提供的传播器把当前 ctx 中的 span 上下文序列化进 header。否则下游 propagator.Extract() 解不出有效 span。
- 必须用
propagation.HeaderCarrier(req.Header)包装 header,这是 OTel 的契约接口,不是普通 map -
Inject()必须在http.Client.Do()前最后一刻执行——如果中间有重试封装、中间件或自定义 transport,要确保它发生在最终发出请求前 - 别用
http.NewRequest()后就立刻Inject(),因为某些 transport 会 clone request,header 变更可能丢失
示例关键行:
立即学习“go语言免费学习笔记(深入)”;
propagator := otel.GetTextMapPropagator() req, _ := http.NewRequestWithContext(ctx, "GET", url, nil) propagator.Inject(ctx, propagation.HeaderCarrier(req.Header)) resp, err := client.Do(req)
HTTP 服务端必须用 propagator.Extract()重建ctx
中间件里从 r.Header 读出 traceparent 后,不能直接 context.WithValue(r.Context(), ...) 就完事。那样只存了 traceID 字符串,没重建 span 关系,下游调用 tracer.Start() 时仍会生成新 trace。
- 必须用
propagator.Extract()从 header 构造带完整 span context 的新ctx - 新
ctx要通过r = r.WithContext(newCtx)显式绑定回 request,否则 handler 拿到的还是原始 context - 优先解析标准
traceparent,fallback 到X-Trace-ID等自定义头时,需手动构造trace.SpanContext并注入,不能跳过 span context 层
gRPC 场景下 metadata.MD 是透传正道
gRPC 不走 HTTP header,得用 metadata.MD。但注意:它不是自动透传的,A→B→C 链路中,B 必须主动从入参读取、再写入 outgoing metadata 才能传给 C。
- 客户端注入:用
metadata.Pairs("traceparent", val)构建md,再用metadata.NewOutgoingContext(ctx, md) - 服务端提取:用
metadata.FromIncomingContext(ctx)获取入参 metadata,再喂给propagator.Extract() - 别用
md.Set("key", "val")—— 它大小写敏感且会覆盖同名键;Pairs才是安全构造方式 - metadata 只支持
string和[]string,别往里塞 struct、ctx 或函数
最易忽略的一点:goroutine 里启动异步操作时,必须显式传入当前 ctx,闭包捕获的可能是旧 context 或 background,导致 traceID 突然为空。


















