http handler 必须用 otelhttp.newhandler() 包装以确保链路追踪完整,它自动解析 traceparent、注入 context;数据库需用 otel-sql 驱动包装,goroutine 必须显式传入带 span 的 context,关键写操作应保 trace_id 但脱离请求生命周期。

HTTP handler 入口必须用 otelhttp 中间件包装
不包装就等于没链路——otelhttp.NewHandler() 不只是加个 span,它会自动从 traceparent header 解析父上下文、创建 root 或 child span,并把带 span 的 context.Context 注入到 http.Request 中。手写 req.Context() + tracer.Start() 很容易漏掉 tracestate、采样标志或 baggage 透传。
常见翻车点:
- 把
otelhttp.NewHandler()套在其他中间件之后(比如鉴权中间件返回了新*http.Request,但没传回 context),导致下游拿到的是原始空 context - 用
http.HandleFunc()直接注册 handler,绕过了中间件链,span 完全丢失 - Gin 用户误用
gin.Engine.Use(otelgin.Middleware(...))后,又在 handler 里手动调tracer.Start(context.Background(), ...),造出孤立 root span
数据库调用必须用 otel-sql 驱动包装
database/sql 原生不读 context 里的 span,哪怕你传了 ctx 给 db.QueryContext(),子 span 也只会挂在当前 goroutine 下,不会关联到 HTTP 请求的 trace 上。
正确做法是用 go.opentelemetry.io/contrib/instrumentation/database/sql 包:
- 调
otel.WrapDriver(driver, otel.WithTracerProvider(tp))包装原始 driver(如pgx/v5或mysql) - 用包装后的 driver 打开
sql.Open(),否则所有 DB span 都是断的 - GORM 用户别只设
db.WithContext(ctx),得确保底层sql.DB是用 otel-wrap 过的 driver 初始化的
goroutine 启动前必须显式传带 span 的 context
Go 的 go fn() 不继承 caller 的 context,更不会自动提取 span。直接 go doWork(),新协程里的 tracer.Start(ctx, ...) 会生成一个无 parent 的 root span,链路当场断裂。
安全写法只有这一种:
- 用
ctx := trace.ContextWithSpan(context.Background(), parentSpan)派生新 context(注意不是context.Background()) - 把该 ctx 显式传进 goroutine:
go func(ctx context.Context) { ... }(ctx) - 别依赖闭包捕获外层变量——如果外层 ctx 是局部变量,且 goroutine 执行晚于函数返回,可能已失效
关键写操作要脱离请求生命周期但保留 span
用户关页面、App 切后台时,gin.Context 会 cancel,GORM 若用该 ctx 做订单写入,大概率报 context canceled 并丢数据。但直接换 context.Background() 又会丢 trace_id,日志和监控对不上。
折中方案是“保 trace、脱生命周期”:
- 从请求 ctx 提取 span:
span := trace.SpanFromContext(reqCtx) - 用
context.Background()派生新 ctx:writeCtx := trace.ContextWithSpan(context.Background(), span) - 再套一层
context.WithTimeout(writeCtx, 3*time.Second)防泄漏 - 确保
defer cancel()在写操作结束后立即执行,别等到 handler 返回
这个细节最容易被忽略:span 能保留,但 timeout 和 cancel 必须由业务代码主动管理,SDK 不会替你兜底。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











