不推荐用context.withvalue传业务参数,因其掩盖依赖、破坏函数签名、导致测试困难、运行时panic频发;仅应传递请求级只读元数据(如traceid、userid),且key须为私有类型、取值必用双判断。

直接用 context.WithValue 传业务参数(比如 userID、db、config),就等于把函数签名藏起来,让调用关系不可见、测试不可靠、重构不敢动。
为什么 context.WithValue 一用就变成隐式全局变量
它不强制你声明依赖,也不参与函数签名检查。一个 handler 里写 ctx.Value(userKey{}).(string),IDE 找不到谁塞的值,单元测试时得手动补全整条 context 链,CI 跑过不代表运行时不 panic——因为漏传、类型错、覆盖、nil 值都只在运行时暴露。
- 第三方库或老 SDK 没透传
ctx,下游直接取到nil,强转就崩 - 两个中间件都用
userKey{},但一个存string、一个存int,下游断言失败静默出错 - 测试里忘了
context.WithValue(context.Background(), userKey{}, "test"),生产才报 panic
context.WithValue 的合法边界在哪
只允许存请求生命周期内**只读、轻量、不可变、与链路强相关**的元数据。不是“能塞进去”,而是“非塞不可且别无他法”。
- ✅ 合法:traceID、requestID、authScope、userID(值类型,非指针)、clientIP
- ❌ 禁止:*User、map[string]string、*sql.DB、log.Logger、配置结构体、计数器、缓存实例
- ⚠️ 危险:struct 指针(栈变量地址可能悬垂)、含 slice/map 字段的 struct(不可比较,
context.WithValue运行时报 panic)
key 用 string 或 int 就是埋雷
Go 编译器不拦你,但 runtime 不保证语义隔离。两个包都写 context.WithValue(ctx, "user_id", ...),后执行的覆盖前一个,谁赢取决于调用顺序,且毫无提示。
- 必须定义未导出类型:
type userKey struct{}或type ctxKey string+const userKey ctxKey = "user" - 空 struct
struct{}是最安全选择:零内存、不可导出、类型唯一 - 绝对不要用
var UserKey = "user_id"—— 它本质还是string,冲突风险没变
取值不带 v, ok := ctx.Value(key).(T) 就是等着 panic
context.Value 返回 interface{},类型擦除是设计使然。强转失败不是 bug,是提醒你:这里不该承载关键逻辑。
- 永远不用单返回值断言:
uid := ctx.Value(userKey{}).(string)→ 一旦为 nil 或类型错,直接 crash - ok 判断后必须有 fallback 或明确错误路径:
if uid, ok := ctx.Value(userKey{}).(string); !ok { return errors.New("missing user_id in context") } - 高频路径(如日志打点)里反复断言?说明该数据不该放 context,该走显式参数
最常被忽略的不是怎么塞值,而是 context 根本没传下去——goroutine 启动漏传、中间件忘了 r = r.WithContext(newCtx)、老 SDK 不支持 context。查不到值时,先看调用链上有没有哪一层悄悄替换了 ctx 或压根没传。











