context.Context 不是用来存业务数据的,而是传递取消信号、超时控制和少量请求级元数据;滥用 WithValue 会导致性能下降、类型不安全、panic 和 goroutine 泄漏。
直接说结论:context.context 不是用来“存业务数据”的,而是用来传递取消信号、超时控制和少量请求级元数据的。滥用 withvalue 是最常见也最危险的错误。
为什么不能把 context 当 map 用
很多人一看到 WithValue 就立刻想塞用户 ID、token、traceID……但 Context 的设计目标不是通用键值存储,它的 Value 查找是链式遍历,深度越深性能越差;更关键的是,它没有类型安全、没有生命周期管理、无法被静态检查——你传进去一个 string,取出来可能是个 int,运行时报 panic: interface conversion: interface {} is string, not int。
真实场景中容易踩的坑:
- 在中间件里用
ctx = context.WithValue(ctx, "user_id", userID),下游却用ctx.Value("user_id").(int)强转,一旦上游没设或类型变了就 panic - 把数据库连接、HTTP client 等长生命周期对象塞进 context,导致 goroutine 泄漏或连接复用混乱
- 用字符串字面量当 key(如
"user_id"),多个包重复定义,互相覆盖又难以发现
正确传递取消和超时信号
这才是 context.Context 的核心价值:让一次请求的所有 goroutine 能被统一中断。比如 HTTP handler 启动了 3 个子 goroutine 去查缓存、DB、第三方 API,只要客户端断开连接,整个链路应立刻退出,不浪费资源。
实操要点:
- 总是从入参接收
ctx context.Context,不要用context.Background()或context.TODO()自行创建(测试或顶层初始化除外) - 超时优先用
WithTimeout或WithDeadline,而不是自己起 timer + channel - 调用下游函数前,记得把当前
ctx传下去,尤其是http.Client.Do、database/sql.QueryContext、time.AfterFunc等明确支持 context 的接口 - 自己写的阻塞操作(比如轮询、sleep),必须显式监听
并返回 <code>ctx.Err()
示例:
func fetchUser(ctx context.Context, id int) (User, error) {
// 数据库查询自动响应 ctx 取消
row := db.QueryRowContext(ctx, "SELECT name FROM users WHERE id = ?", id)
var name string
if err := row.Scan(&name); err != nil {
return User{}, err
}
return User{Name: name}, nil
}
安全地传少量元数据
如果真要传数据(比如 traceID、requestID),必须满足两个条件:一是只读、不可变;二是用私有类型做 key,避免冲突。
正确写法:
- 定义 key 类型:
type requestIDKey struct{},而不是const RequestIDKey = "req_id" - 传值:
ctx = context.WithValue(ctx, requestIDKey{}, "abc123") - 取值:
if rid, ok := ctx.Value(requestIDKey{}).(string); ok { ... }
注意:requestIDKey{} 是空结构体,零内存占用,且每次构造都是新类型实例——这能防止不同包误用同一字符串 key 导致覆盖。
别忽略 cancel 函数的调用时机
WithCancel、WithTimeout、WithDeadline 都返回一个 cancel func(),它必须被调用,否则底层的 done channel 不会关闭,goroutine 和 timer 都不会释放。
典型错误:
- 在 defer 中调用
cancel(),但函数提前 return 了,defer 没执行 - 把
cancel传给子 goroutine,但父 goroutine 已经退出,子 goroutine 拿到的是已失效的函数 - 忘记调用,尤其在 error early return 分支里漏掉
建议写法:在函数入口立即 defer,哪怕后面有多个 return:
func handleRequest(ctx context.Context) {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel() // 这里保证一定执行
if err := doSomething(ctx); err != nil {
return // cancel 仍会执行
}
}
真正难的不是怎么写,而是判断该不该用 context —— 一次 HTTP 请求、一个 RPC 调用、一个后台任务,这些是明确边界;而跨 service、跨进程、或者需要持久化存储的数据,context 就不该出现。

















