应使用otel.tracer替代自定义tracer接口,因其为标准接口且兼容所有主流sdk;模块内统一调用tracer.start(ctx, "name"),通过依赖注入传递tracer,避免全局变量或init()初始化;http出向请求须用otel.gettextmappropagator().inject注入w3c标准字段,禁用手动set header;grpc需用otelgrpc.withpropagators配置;span.end()应显式调用并校验isvalid,长周期goroutine每次处理需新建span;gin/echo等框架须用对应官方otel中间件适配器,禁用otelhttp.withfilter默认过滤。

用 otel.Tracer 替代自定义 tracer 接口
Go 模块里硬编码一个 Tracer 接口实现,后续想换 OpenTelemetry 或对接 Jaeger 就得改一堆地方。直接依赖 go.opentelemetry.io/otel 的标准 otel.Tracer,它本身是接口,且被所有主流 SDK(otlpgrpc、jaeger、zipkin)兼容。
实操建议:
- 模块内所有埋点统一调用
tracer.Start(ctx, "handler.process"),不自己 new struct - 把
tracer作为依赖注入进 service 层,别从全局变量取 - 避免在
init()里初始化 tracer —— 单元测试时无法替换,也容易触发提前加载导致配置未生效
Context 传递必须显式用 otel.GetTextMapPropagator().Inject
HTTP 中间件里拿到的 req.Context() 默认不含 trace context,如果只靠 req.Header.Get("traceparent") 手动解析,会漏掉 baggage 和 vendor 扩展字段,下游服务看到的 trace 就断了。
实操建议:
- 出向 HTTP 请求前,用
otel.GetTextMapPropagator().Inject(req.Context(), req.Header)注入标准 W3C 字段 - 别用
req.Header.Set("traceparent", ...)拼字符串 —— 缺少大小写规范、校验逻辑,某些网关(如 Envoy)会静默丢弃 - gRPC 场景下改用
otelgrpc.WithPropagators(otel.GetTextMapPropagator())选项,而不是手动塞 metadata
避免在 defer 中调用 span.End() 导致 context 泄漏
常见写法 defer span.End() 看似简洁,但若 span 是从父 context 创建、而父 context 已 cancel 或超时,defer 执行时 span 可能已失效,部分 exporter(如 OTLP over gRPC)会报 rpc error: code = Canceled desc = context canceled 并静默丢弃 span。
实操建议:
- 在函数返回前显式调用
span.End(),确保它和对应 context 生命周期对齐 - 若必须 defer,先判断
span.SpanContext().IsValid()再结束 - 对长生命周期 goroutine(如消息消费循环),每次处理新消息都应新建 span,不要复用上一次的
otelhttp.NewHandler 不能直接套在 Gin/Echo 的中间件链里
Gin 的 c.Next() 和 Echo 的 next(ctx) 不等价于标准 http.Handler.ServeHTTP 调用路径,直接把 otelhttp.NewHandler 当中间件注册,会导致 span 名称全是 GET /、丢失路由参数、甚至 panic(因内部强转 http.ResponseWriter 失败)。
实操建议:
- Gin:用
gin-middleware官方适配器(otelgin.Middleware),或在c.Request.URL.Path上做路由匹配后手动 start span - Echo:优先用
otelecho.Middleware,别自己 wrapecho.HTTPErrorHandler—— 错误处理阶段的 span 必须和原始请求 span 关联,否则链路断裂 - 所有框架,都禁用
otelhttp.WithFilter里的默认 4xx/5xx 过滤 —— 业务返回 401、422 也是有效链路环节,不该被跳过
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











