结论:context.WithValue 设计目的是跨 API 边界传递请求范围的元数据(trace ID、认证身份、截止时间类信息),官方文档明确警告:不应把函数的可选参数塞进去。滥用会让函数签名说谎——依赖藏在 ctx 里,调用契约不可见,测试和重构都变难。

展开:判断边界的三条经验:1)"没有它函数语义是否改变"——改变则必须是显式参数(如数据库连接、配置、业务选项);不改变、纯横切关注点(日志字段、追踪、鉴权身份)才适合放 ctx。2)"传递路径是否跨越多层 API"——request-scoped 数据的特征是要穿过十几层你不控制签名的中间件,显式传参不现实;自己模块内两三层的传递用参数。3)键必须用自定义类型避免冲突:type ctxKey string; const userKey ctxKey = "user",用 string/int 字面量当 key 会和其他包撞键。易错点:1)存可变结构体后多方并发修改——ctx 传递的数据应当不可变;2)拿 ctx 当万能袋子导致内存滞留——长生命周期的 ctx(如后台任务复用请求的 ctx)拖着整个 value 链不放;3)context.Backgroundcontext.TODO 的语义区分——TODO 是"还没想好谁给我 ctx"的占位,静态检查会盯。

type ctxKey string
const traceKey ctxKey = "trace-id"

ctx := context.WithValue(r.Context(), traceKey, id)
if v, ok := ctx.Value(traceKey).(string); ok { log(v) }

追问方向:OpenTelemetry 的 baggage 与 ctx value 的关系、Go 1.21 的 WithoutCancel 解决什么问题(异步任务继承值但不受取消影响)。