context.Context必须作为函数第一个参数,不可省略或挪位;禁用context.WithValue传业务数据;下游调用前须用tracer.Start延续span;goroutine需显式传ctx。

Context 必须作为第一个参数传入函数
Go 官方约定和生态共识要求 context.Context 必须是函数的第一个参数,且不可省略或挪到中间。这不是风格问题,而是工具链(如 tracing SDK、linter)依赖此约定做自动注入和 span 关联。
常见错误是把 ctx 放在参数末尾,或用 struct 包裹后传入——这会导致 otel.Tracer.Start 无法从调用栈中提取父 span,链路直接断裂。
- ✅ 正确:
func DoSomething(ctx context.Context, id string, timeout time.Duration) error - ❌ 错误:
func DoSomething(id string, ctx context.Context) error - ❌ 错误:
func DoSomething(req MyRequest) error(MyRequest里含ctx字段)
不要用 context.WithValue 传递业务数据
context.WithValue 的唯一合理用途是传递跨层的、与请求生命周期强绑定的元数据(如 traceID、userID),而不是替代函数参数。滥用它会让函数签名不透明、难以测试、且容易因 key 冲突或类型断言失败导致 panic。
链路追踪本身不依赖 context.WithValue —— OpenTelemetry 等 SDK 会自动从 ctx 中提取 span 并延续,你只需确保每次调用下游时都传入更新后的 ctx。
立即学习“go语言免费学习笔记(深入)”;
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- ✅ 合理:
ctx = context.WithValue(ctx, keyTraceID, "abc123")(仅用于调试或透传已有的 trace 上下文) - ❌ 风险:
ctx = context.WithValue(ctx, keyDBConfig, dbCfg)(应改为显式参数或依赖注入) - ⚠️ 注意:
context.WithValue的 key 类型必须是 unexported 类型,否则不同包间易冲突;推荐用type traceKey struct{}定义
下游调用前必须用 WithSpan / WithContext 延续 trace
当你调用另一个函数(尤其是跨服务或跨 goroutine)时,如果没把当前 span 注入新 ctx,新 span 就会变成 root,链路就此断开。OpenTelemetry 提供 trace.ContextWithSpan 或更常用的 tracer.Start 返回带 span 的 ctx。
关键不是“有没有传 ctx”,而是“传的 ctx 是否包含当前 span”。很多开发者只做了 ctx = context.WithTimeout(ctx, d),却忘了 span 仍停留在旧 ctx 里。
- ✅ 正确:
span := tracer.Start(ctx, "db.query") defer span.End() ctx = span.SpanContext().Context()
或更简洁地:ctx, span := tracer.Start(ctx, "db.query") defer span.End()
- ❌ 断链:
ctx, _ := context.WithTimeout(ctx, 5*time.Second)(没关联 span) - ⚠️ 注意:HTTP client、database/sql、gRPC client 等主流库已内置 OpenTelemetry 支持,启用后会自动从
ctx提取 span 并注入 header/propagation,无需手动处理;但前提是你的入口函数传入的ctx已被正确初始化(例如 HTTP handler 中从 request context 提取)
goroutine 启动时必须显式传递 Context
Go 的 context.Context 不随 goroutine 自动传播。你在主 goroutine 创建的 ctx,不会自动出现在新 goroutine 里。若新 goroutine 执行异步操作(如日志上报、缓存刷新),又没传 ctx,它的 span 就会脱离主链路,甚至可能因 parent ctx cancel 导致 panic。
尤其注意 go func() { ... }() 这种匿名启动方式——闭包捕获的 ctx 是变量引用,而非值拷贝,一旦原 ctx 被 cancel,所有闭包里的 ctx 都失效。
- ✅ 安全:
go func(ctx context.Context) { // 使用传入的 ctx }(ctx) - ✅ 更推荐:
go doAsyncWork(ctx)
(doAsyncWork函数签名含ctx context.Context) - ❌ 危险:
go func() { // 直接用外层 ctx 变量 doSomething(ctx) // ctx 可能已被 cancel }()
ctx 传递,整条链就断了。

















