WithValue传值须用私有类型作key防冲突,如type userKey struct{};仅限请求生命周期数据,禁用于配置、业务参数或频繁修改变量,避免黑盒调用与僵尸goroutine。

WithValue 传值必须用自定义 key 类型
直接用 string 或 int 当 key,不同包之间极易冲突。比如两个中间件都用 "user_id" 作为键名,ctx.Value("user_id") 取到的可能是对方塞进去的值。
正确做法是定义私有类型:
type userKey struct{}
然后统一用它存取:
ctx = context.WithValue(ctx, userKey{}, &User{ID: 123})
u := ctx.Value(userKey{}).(*User)
- key 类型不导出(小写开头),能天然隔离包间命名空间
- 哪怕多个包都定义了
userKey struct{},它们在类型层面也不等价 - 避免用
context.WithValue(ctx, "user_id", ...)这类裸字符串
值传递只适用于 request-scoped 数据
WithValue 不是通用参数传递机制,它只承载与当前请求生命周期一致的数据,比如用户身份、trace ID、租户标识。
以下场景不适合用 WithValue:
- 函数可选配置项(该用结构体参数或函数选项模式)
- 业务逻辑强依赖的输入参数(应显式声明为函数参数)
- 需要频繁修改的变量(
Value是只读的,改值只能换新 context)
误用会导致调用链变“黑盒”:函数签名看不出依赖什么数据,调试和测试成本陡增。
跨 goroutine 传递时注意 Done 通道监听时机
如果子 goroutine 在启动后才开始监听 ctx.Done(),而父 context 已提前取消,就会错过信号,变成僵尸 goroutine。
典型错误写法:
go func() {
time.Sleep(100 * time.Millisecond) // 延迟进入
select {
case
- 这 100ms 窗口内取消信号已发出,但 goroutine 还没开始监听
- 正确方式是:一进入就检查
ctx.Err(),或立即监听ctx.Done() - 尤其要注意 HTTP handler 中启的 goroutine,请求可能在你 sleep 时已被客户端断开
不要把 context 存进 struct 字段里
Go 官方明确禁止将 context.Context 保存为结构体成员。它不是状态,而是每次调用的瞬态输入。
反模式示例:
type Service struct {
ctx context.Context // ❌ 错误:生命周期不可控,无法响应超时/取消
}
- struct 实例可能长期存活,但 context 生命周期只属于某次请求
- 一旦该 context 被取消,struct 里存的仍是旧引用,后续调用会误判状态
- 所有方法都应把
ctx作为第一个参数显式传入,如func (s *Service) Do(ctx context.Context, ...)
最易被忽略的一点:即使你没主动存,ORM 或 client 库内部缓存了 context,也要确认它是否随每次调用重新绑定——否则超时控制就形同虚设。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











