根本原因在于未初始化SDK:必须在main()开头调用otel.SetTracerProvider(tp),否则otel.Tracer().Start()返回noop Span,无trace ID、无parent、SpanContext().IsValid()为false;HTTP需显式配置传播器并用otelhttp.NewHandler包裹handler,异步goroutine中Span必须显式End()。

OpenTelemetry 在 Go 中不是靠“语言学习”实现的,而是通过显式引入 SDK、配置导出器、注入上下文完成的。所谓“语言学习”容易误解为编译器或运行时自动支持——Go 本身不自动注入追踪,必须手动埋点或借助框架插件。
用 go.opentelemetry.io/otel 初始化 SDK 是前提
没初始化 SDK,调用 Tracer().Start() 只会返回空操作(no-op)的 Span,日志里看不到数据,后端也收不到。
- 必须调用
otel.InitTracerProvider()或手动构建trace.NewTracerProvider()并设为全局 - 导出器(如
otlpgrpc.NewClient())要配对使用,否则 Span 被丢弃而不报错 - 常见错误:
Tracer("my-service")写了但没设otel.SetTracerProvider(tp),结果所有 Span 都是nil的 - 推荐在
main()开头做初始化,且确保Shutdown()在程序退出前调用,否则最后一批 Span 可能丢失
HTTP handler 中手动注入 context.Context 是关键
Go 的 net/http 不自动传递 Span 上下文,你得自己从请求中提取 SpanContext,再塞回 context.Context 供后续使用。
- 用
otelhttp.NewHandler()包裹 handler 最省事,它自动做提取、创建 server span、注入 context - 若手写 handler,需调用
otel.GetTextMapPropagator().Extract(r.Context(), r.Header)获取远端 Span 信息 - 别直接用
r.Context()启动新 Span——那会断链;必须用提取后的 context 调用tracer.Start(ctx, "...") - 注意:
otelhttp默认只处理 root span,子 Span 还得你自己在业务逻辑里用span.AddEvent()或span.SetAttributes()
otelhttp 和 otelgrpc 插件的行为差异影响链路完整性
HTTP 客户端和 gRPC 客户端的 OpenTelemetry 插件默认行为不同,混用时容易漏掉中间跳转。
-
otelhttp.NewClient()默认不传播 context,需显式传入带 Span 的context.WithValue()或用http.RoundTripper包装器 -
otelgrpc.Dial()会自动注入 client span,但前提是调用方 context 已含有效 Span;否则它起一个孤立的 client span - 跨服务调用时,务必确认 header 里有
traceparent字段——没有说明 propagator 没生效或服务端没正确 extract - gRPC 场景下,
otelgrpc.UnaryClientInterceptor()和otelgrpc.UnaryServerInterceptor()必须成对启用,否则链路在 gRPC 边界断裂
本地验证时 stdout 导出器比 OTLP 更直观
开发阶段别急着连 Jaeger 或 OTLP endpoint,先用 stdout 看 Span 是否生成、父子关系是否正确,避免网络或配置干扰判断。
立即学习“go语言免费学习笔记(深入)”;
- 替换导出器为
stdout.NewExporter(stdout.WithPrettyPrint()),启动后终端直接打印 JSON 格式 Span - 重点看
ParentSpanID和TraceID是否一致——不一致说明上下文没传下去 - 如果只有 root span 没 child span,大概率是业务函数没接收并传递
context.Context参数 -
otel.WithSpanKind(trace.SpanKindServer)和trace.SpanKindClient必须按角色设置,否则链路图里方向反了


















