context.withvalue 本身线程安全,但传入的值若为非线程安全类型(如普通 map、slice)或生命周期超出 context,将引发竞态或泄漏;应传不可变轻量数据,键须可比较且避免跨包冲突。

Context.WithValue 本身不引发读写冲突
context.WithValue 返回的新 Context 是不可变的(immutable):它只是对父 Context 的封装,内部不修改任何共享状态。调用 WithValue 不会触发对 map、slice 或其他可变结构的并发写入,所以这一步本身是线程安全的。
但问题常出在「值本身」——如果你传进去的是一个非线程安全的结构(比如普通 map、slice、自定义 struct 含未加锁字段),后续多个 goroutine 并发读写这个值,就会触发竞态。这不是 Context 的错,而是你把不安全的东西塞进去了。
- ✅ 安全值示例:
string、int、time.Time、struct{ ID string }(只读字段) - ❌ 危险值示例:
map[string]int、[]byte(被多个 goroutine 修改)、*sync.Mutex(误以为能共享锁) - ⚠️ 特别注意:不要传指针指向可变数据,除非你明确控制了它的并发访问(比如配合
sync.RWMutex)
传 map 或 cache 时必须用 sync.Map 或加锁
常见错误是这样写:
type ctxKey struct{}
cache := make(map[string]int)
ctx := context.WithValue(parent, ctxKey{}, cache) // ❌ 普通 map 并发读写会 panic
一旦多个 goroutine 从 ctx.Value(key) 拿到这个 map 并同时读写,Go runtime 会直接 panic 报 fatal error: concurrent map read and map write。
- ✅ 推荐方案:用
sync.Map替代原生map,它专为并发设计,读写安全 - ✅ 替代方案:把 cache 封装成 struct,内部用
sync.RWMutex控制读写 - ❌ 避免方案:在每个 goroutine 里自己加锁操作原生
map—— 锁不在同一作用域,根本不起作用
WithValue 的键类型必须可比较且避免冲突
用自定义 struct 当 key 看似安全,但若没导出字段或用了指针/切片,会导致 Value() 查不到值(返回 nil),进而可能引发空指针 panic 或逻辑跳过。更隐蔽的问题是:不同包定义相同结构体字面量,会被 Go 视为不同类型,导致 key 匹配失败。
- ✅ 推荐键类型:
struct{}、string、导出的未嵌套 struct(如type userCtxKey struct{}) - ✅ 最佳实践:定义包级私有 key 类型,避免跨包误用:
type userKey struct{}+var UserKey = userKey{} - ❌ 避免键类型:
interface{}、[]byte、含指针或 map 字段的 struct(不可比较)
真正危险的是「值生命周期」超出 Context 生命周期
WithValue 存的值不会被 Context 自动释放,如果值包含大内存对象(如未关闭的 *os.File、长生命周期的 channel),而 Context 被取消后该值仍被其他 goroutine 持有,就可能造成资源泄漏或状态不一致。
- ✅ 应对方式:只存轻量、无副作用的元数据(如请求 ID、用户 ID、traceID)
- ✅ 若必须传资源句柄,确保调用方明确负责关闭,并在
Done()后不再使用该值 - ⚠️ 关键提醒:Context 取消 ≠ 值自动销毁;
Value()返回的仍是原引用,不带所有权语义
Context 是否线程安全,却忘了它只是个“信封”,里面装的东西才决定会不会出事。传什么、怎么用、何时丢弃,比怎么创建 Context 更关键。











