链路追踪必须显式透传 context,HTTP 需用 traceparent 头配合 Inject/Extract,gRPC 需 metadata 拦截器,自定义 key 须为未导出空结构体,中间件须替换 request.Context(),goroutine 中需显式传入派生 ctx。

Context 不能直接传递链路信息,必须配合 W3C traceparent 头或 gRPC metadata 显式透传,否则跨服务时必然断链。
HTTP 请求中必须手动注入和解析 traceparent
OpenTelemetry Go SDK 不会自动把 context.WithValue 里的 traceID 发到下游。你存了没用,下游收不到。
- 上游发送请求前,必须调用
otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(req.Header)),它会把traceparent写进req.Header - 下游 HTTP handler 收到请求后,必须调用
otel.GetTextMapPropagator().Extract(ctx, propagation.HeaderCarrier(r.Header)),不能自己r.Header.Get("traceparent")然后手动塞进 context - 务必用
traceparent: 00-abc123-def456-01格式,别写X-Trace-ID—— OpenTelemetry 默认不识别它 - 如果对接 Zipkin/Jaeger,需注册复合 propagator:
propagation.NewCompositeTextMapPropagator(propagation.B3{}, propagation.TraceContext{})
gRPC 场景下必须用 metadata.MD + 拦截器
gRPC 不序列化 context.Value,WithValue 在客户端和服务端之间完全失效。
- 客户端发起调用前,构建
md := metadata.Pairs("traceparent", "00-..."),再用grpc.HeaderMD(md)透传 - 服务端必须注册 unary 或 stream 拦截器,在拦截器里调用
metadata.FromIncomingContext(ctx)解析,并用ctx = metadata.NewIncomingContext(ctx, md)绑回 - 漏掉拦截器或没绑回
ctx,后续所有ctx.Value()都是空的
自定义 key 类型必须全局唯一且不可导出
用 "trace_id" 字符串当 key 是最常见断链原因——跨包时内存地址不同,ctx.Value("trace_id") 永远返回 nil。
立即学习“go语言免费学习笔记(深入)”;
- 正确定义:
type traceKey struct{}(未导出、空结构体),再声明var traceKeyKey = traceKey{} - 存取严格配对:
context.WithValue(ctx, traceKeyKey, id)和ctx.Value(traceKeyKey) - key 类型名可以重复,但变量实例必须全局唯一;IDE 能跳转、编译器能校验,比字符串安全得多
中间件里注入后必须显式替换 request.Context()
很多链路追踪中间件看似写了 context.WithValue(r.Context(), ...),但忘了最关键的一步:把新 context 绑回 request。
- 必须执行
r = r.WithContext(newCtx),再调用next.ServeHTTP(w, r) - 否则 handler 拿到的仍是原始
r.Context(),里面没有你塞的 traceID - goroutine 中使用 traceID 也必须显式传入
ctx,闭包捕获的可能是旧值或 nil
真正难的不是怎么写那几行 inject/extract 代码,而是确保每个 I/O 点(HTTP client、gRPC client、DB query)都用了派生后的 ctx,且所有中间件、拦截器、goroutine 启动点都完成了 context 的透传与绑定。漏掉任意一环,链路就断在那儿了。


















