
otel.Tracer() 返回 nil 怎么办
根本原因只有一个:没调用 otel.SetTracerProvider,或者调用时传入的 TracerProvider 初始化失败被静默忽略。
常见表现是:otel.Tracer("my-service").Start(ctx, "doWork") 看似执行了,但实际走的是 noop 实现——span 里没有 trace ID、没有 parent、span.SpanContext().IsValid() 返回 false,日志里也看不到任何 span 数据。
- 必须在
main()开头就初始化sdktrace.NewTracerProvider,并立即调用otel.SetTracerProvider(tp) - 别把初始化逻辑塞进
init()函数——Go 的init执行顺序不可控,HTTP handler 可能比 tracer provider 更早 ready - 本地调试优先用
stdouttrace.NewUnstarted()+ 显式.Start(),终端直接看到 JSON 格式的 span,比查 Jaeger 或日志快得多 - 检查错误:exporter 创建失败(比如 OTLP 地址不通)不会 panic,但会导致
tp为 nil,后续otel.Tracer()必然返回 nil
HTTP 请求链路断在第一个服务怎么办
典型现象是:前端发请求带了 traceparent,但下游服务收不到,所有 span 都是独立 root span,链路“断开”。本质是 propagation 没配对或没生效。
net/http 默认不解析/注入 trace header,必须显式配置传播器,并确保中间件包裹正确。
立即学习“go语言免费学习笔记(深入)”;
- 初始化时必须调用
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.B3{})),兼容新老系统(B3 是旧版 Zipkin/Jaeger 常用格式) - 用
otelhttp.NewHandler包裹你的http.Handler,不是http.HandleFunc;且必须在路由注册前完成包裹 - 如果用 Gin/Echo,别混用
otelhttp.NewHandler和框架自带的 otel 中间件(如otelgin.Middleware),否则会双 span;推荐只用框架中间件,并在其中手动调用otel.GetTextMapPropagator().Inject写回 response header - 验证 header:curl 测试时加
-H 'traceparent: 00-12345678901234567890123456789012-1234567890123456-01',再看下游服务是否能从r.Context()提取出有效 span
异步 goroutine 里的 span 总是漏掉或内存上涨
Go 里最隐蔽的坑:go func() { ... } 里创建的 span 没 End(),导致 span 卡在 batch exporter 缓冲区,几小时后内存缓慢爬升甚至 OOM。
Span.End() 不是可选操作——它触发采样判定、属性合并、事件 flush。漏掉就等于丢数据,还拖慢整个 exporter。
- 所有
Tracer.Start()必须配对span.End(),哪怕在 defer 里也得确保执行;defer span.End()是底线,不是最佳实践 - goroutine 中不能直接用外部 request context,必须用
trace.ContextWithSpan显式传递当前 span 到子 context - 避免在 defer 里写
span.End()后又 panic —— panic 会跳过 defer,span 就永远没结束;可用recover()补救,或改用span.End(trace.WithStackTrace(true))辅助定位 - 导出器超时卡住请求?OpenTelemetry Go SDK 默认用
http.DefaultClient,timeout 是 30s;开发环境建议设短超时,或换用otlptracehttp.New(..., otlptracehttp.WithTimeout(5*time.Second))
Span 名称全是 “HTTP GET” 怎么办
这是 otelhttp.NewHandler 的默认行为:它只取 HTTP method 和 protocol,不解析路由模板。Gin/Echo 的动态路由(如 /api/v1/users/:id)无法被自动识别,span name 就退化成方法名。
想看到语义化名称(如 "GET /api/v1/users/{id}" 或 "User.GetByID"),得主动控制。
- 用
otelhttp.WithSpanNameFormatter自定义 name,例如func(r *http.Request, _ *http.Response) string { return r.Method + " " + r.URL.Path } - 更推荐在框架中间件里手动创建子 span,并调用
span.SetName("GET /api/v1/users")+span.SetAttributes(attribute.String("http.route", c.FullPath()))(Gin 中c.FullPath()返回注册的 pattern) - 别重复 start span:如果已用
otelhttp.NewHandler,就别再在 handler 里手动tracer.Start(),否则一个请求生成两个 span,时间线错乱 - 注意 span 层级:根 span 应该是 HTTP 入口,业务逻辑 span 应作为其 child,用
trace.WithSpanContext(parentCtx)确保父子关系正确
真正难的不是写通第一行 Tracer.Start(),而是让每个 goroutine、每个 HTTP 跳转、每个第三方调用都严格遵循 context 传递和 span 生命周期管理。稍有松懈,链路就断、数据就丢、内存就涨——这些点不在文档首页,但线上故障里十次有八次栽在这儿。


















