Go的context本身不实现全链路追踪,仅提供传递请求范围值和取消信号的能力;真正链路追踪需依赖OpenTelemetry等第三方库将Span注入context,否则传入的只是空壳。

Go 的 context 本身不实现全链路追踪,它只提供传递请求范围值和取消信号的能力;真正做链路追踪得靠第三方库(如 opentelemetry-go)在 context 中存取 Span,否则你传的只是空壳。
为什么不能直接用 context.WithValue 塞 traceID?
能塞,但容易出错:手动管理 traceID 和 spanID 容易漏传、覆盖、类型错配;context.WithValue 的 key 必须是全局唯一接口或指针,用字符串当 key(比如 "trace_id")会导致不同包之间冲突;而且没有自动采样、上报、父子 span 关联逻辑。
实操建议:
- 永远用类型安全的 key,例如
type traceKey struct{},再定义var traceKeyKey = traceKey{} - 不要自己拼接
traceID-spanID,让 SDK 自己生成并维护SpanContext - 如果临时调试,可用
log.Printf("traceID: %s", traceID)打印,但别把它当成追踪方案
opentelemetry-go 怎么把 Span 塞进 context?
核心是 trace.Span 实现了 context.Context 的携带能力,通过 trace.ContextWithSpan 把当前 span 注入 context,并用 trace.SpanFromContext 取出。这个过程不是“存字符串”,而是把实现了 trace.Span 接口的对象挂到 context 上。
常见错误现象:SpanFromContext 返回 nil —— 通常因为上游没调用 Start,或者用了错误的 context(比如用了 context.Background() 而非传入的 ctx)。
实操建议:
- 入口处(如 HTTP handler)调用
tracer.Start(ctx, "handler"),拿到span和新ctx - 后续所有函数都接收并传递这个
ctx,而不是重新context.Background() - 跨 goroutine 时,必须显式传
ctx,go fn(ctx),不能依赖闭包捕获旧 context - 数据库调用、HTTP client 请求等,要使用支持 otel 的封装(如
go.opentelemetry.io/contrib/instrumentation/database/sql),它们会自动从 ctx 提取 span 并创建子 span
HTTP 服务间传播 trace context 需要做什么?
靠 W3C Trace Context 标准,即在请求头里透传 traceparent(必需)和 tracestate(可选)。Go 的 otelhttp 中间件会自动从入向请求头提取、注入 context,并在出向请求头写回。
关键点:
- 服务 A 发起 HTTP 请求前,必须用
http.RoundTripper包装器(如otelhttp.NewTransport),否则traceparent不会自动加到 header - 服务 B 的 handler 必须用
otelhttp.NewHandler包裹,否则无法解析traceparent并生成初始 span - 手动构造 HTTP client request 时,要用
trace.Inject显式写 header,例如:carrier := propagation.HeaderCarrier(r.Header)→propagator.Inject(ctx, carrier) - gRPC 同理,用
otelgrpc.Interceptor,别自己解析 metadata
最容易被忽略的是:Span 的生命周期必须与业务逻辑严格对齐——start 在处理开始前,end 在返回响应后(包括 defer recover)。漏掉 span.End() 或提前 end,会导致链路断裂、指标不准、内存泄漏(span 内部有 timer 和属性 map)。


















