gRPC调用必须用metadata.MD透传上下文,context.WithValue仅进程内有效;客户端需用metadata.Pairs构造并metadata.AppendToOutgoingContext注入,服务端用metadata.FromIncomingContext提取。

gRPC 调用必须用 metadata.MD,不能靠 context.WithValue
context.WithValue 写入的值只存在当前进程内存里,gRPC 请求发出去时根本不会被序列化——下游服务收到的是全新初始化的 context.Context,你塞进去的 userKey、traceID 全是空的。常见错误是中间件里写 ctx = context.WithValue(ctx, userKey{}, u),handler 里直接 ctx.Value(userKey{}) 取值,单服务跑得通,一跨 gRPC 就 panic。
真正能跨进程透传的只有 metadata.MD,它是 gRPC 协议层定义的键值对容器,会自动编码进 HTTP/2 headers。
- 客户端必须用
metadata.Pairs("x-request-id", reqID, "tenant-id", tenant)构造,再调用metadata.AppendToOutgoingContext(ctx, md)注入 - 千万别用
metadata.NewOutgoingContext(ctx, md)——它会丢掉原ctx的Done()和超时信号 - 服务端用
metadata.FromIncomingContext(ctx)获取,但注意:只有真正由 gRPC transport 解析过的请求才有值;单元测试用context.Background()或中间件没传 ctx 都会导致返回 nil
HTTP 调用必须走标准 Header,不是自定义字段
HTTP 没有内置上下文序列化机制,必须手动把元数据提取出来,写进请求头。但不是随便写个 X-Trace-ID 就行——OpenTelemetry Go SDK 默认只识别 traceparent 这个 W3C 标准头。
错误写法:req.Header.Set("X-Trace-ID", "abc123");正确流程是:
立即学习“go语言免费学习笔记(深入)”;
- 客户端用
otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(req.Header))注入 - 服务端用
otel.GetTextMapPropagator().Extract(ctx, propagation.HeaderCarrier(r.Header))解析 - 如果对接 Jaeger/Zipkin 老系统,得注册复合 propagator:
propagation.NewCompositeTextMapPropagator(propagation.B3{}, propagation.TraceContext{}) - 敏感字段如 token 建议加
grpc-encoding-前缀,避免被默认日志 dump 出明文
context.WithValue 只能用于本进程内,且 key 必须类型安全
哪怕不跨服务,context.WithValue 也极易出错:用字符串当 key 会全局冲突,类型断言失败直接 panic,链式调用还会产生嵌套 valueCtx 影响性能。
安全做法是:
- key 必须定义为未导出类型,比如
type userIDKey struct{},然后用userIDKey{}实例作 key - 取值必须封装函数,做空值和类型检查:
func UserIDFromCtx(ctx context.Context) (string, bool) - 多个字段别反复
WithValue,打包成结构体一次性注入:ctx = context.WithValue(ctx, requestMetaKey{}, &RequestMeta{ID: "123", TZ: "Asia/Shanghai"}) -
WithValue只适合只读、轻量、请求生命周期内的元数据;不要传指针、闭包、大对象,更不能替代函数参数
跨服务链路中,B 服务必须主动透传,不能依赖自动继承
gRPC 的 metadata.MD 和 HTTP 的 Header 都不具备“自动跨跳传播”能力。A → B → C 链路中,B 收到 A 的 metadata 后,必须显式读取、可能修改、再重新注入到发给 C 的请求里。
典型漏点:
- B 服务的 handler 拿到
ctx后,没调用metadata.FromIncomingContext提取原始 metadata - 提取后没用
metadata.AppendToOutgoingContext把必要字段(如x-request-id、traceparent)带过去 - 写了
md.Set("X-Request-ID", id),但大小写不一致(X-Request-IDvsx-request-id),下游收不到 - 在异步任务或 goroutine 中用了旧的
ctx,没把新注入 metadata 的 ctx 传进去
最隐蔽的问题是:你以为透传了 traceID,其实只传了 ID 字符串,没传 trace-flags 和 parent-span-id,下游 OpenTelemetry SDK 无法生成合法子 span,链路照样断裂。


















