Context不能跨网络透传,必须通过HTTP Header(traceparent)或gRPC metadata显式传递;需用OpenTelemetry标准格式与传播器,且所有I/O操作必须使用新context。

context.Context 本身不能跨网络透传
直接把 context.Context 塞进 HTTP body、gRPC payload 或 JSON 字段里,下游服务拿到的只是一个空壳——Done() 通道已关闭或根本未初始化,Deadline() 返回 false,Err() 永远是 nil。控制信号彻底丢失,这不是传输失败,而是设计使然:Context 是进程内机制,不序列化、不跨边界。
HTTP 请求必须靠 Header 显式传递 traceparent 等字段
W3C TraceContext 是唯一被 OpenTelemetry Go SDK 默认识别的标准。手写 X-Trace-ID: abc123 不起作用;必须用 traceparent: 00-abc123-def456-01 这种格式,含 trace-id、span-id、采样标志(01 表示采样)和 trace-flags。
- 客户端注入:调用
otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(req.Header)),不是手动req.Header.Set() - 服务端解析:用
otel.GetTextMapPropagator().Extract(ctx, propagation.HeaderCarrier(r.Header)),不是r.Header.Get("traceparent")后自己拼 context - 别漏掉超时头:HTTP 层还需同步透传
X-Request-Timeout或自定义 deadline 头,下游才能重建带 Deadline 的context.WithDeadline
gRPC 必须用 metadata.MD + 拦截器,不能只改 context
gRPC 不会自动读取或序列化 context.Value,metadata.MD 是专为此设计的载体。但注意它只在单次 RPC 调用中有效,A→B→C 链路需 B 主动提取、合并、再透传。
- 客户端:构建
md := metadata.Pairs("traceparent", "00-...", "x-request-id", "req-123"),再用grpc.Metadata(md)作为CallOption - 服务端拦截器:调用
metadata.FromIncomingContext(ctx)取值,再用context.WithValue注入新 key(key 必须是私有类型,如type traceKey struct{}) - 大小写敏感:gRPC 自动将 key 转为小写,
X-Request-ID会变成x-request-id,前后端命名要统一
异步 goroutine 里别直接传 r.Context()
HTTP handler 中的 r.Context() 一结束就触发取消,但日志上报、消息投递这类后台任务不该被请求生命周期绑架。
- 正确做法:用
context.WithoutCancel(r.Context())创建新 ctx,保留Value(如 traceID、userID),丢弃Done通道 - 如需独立超时:再套一层
context.WithTimeout(context.WithoutCancel(r.Context()), 30*time.Second) - 绝对不要在 goroutine 外部调用
cancel(),否则可能误杀其他共享同一 ctx 的 goroutine
真正容易被忽略的是:**所有 I/O 操作点都必须显式接收并使用新 context**。比如数据库查询、HTTP client.Do、gRPC client.Call,如果函数签名没带 ctx 参数,或者你调用时传了旧 ctx 或 context.Background(),那前面所有 header/metadata/WithValue 的工作全白费。信号传播是端到端链条,断一环,整条链就失效。











