不能直接用全局变量传请求ID或用户信息,因为并发下会互相覆盖且生命周期错位;context.Context通过不可变键值对和明确生命周期解决该问题,其WithValue应仅用于框架元数据传递,key须为自定义类型,value须无副作用,且避免嵌套滥用。

为什么不能直接用全局变量传请求ID或用户信息
因为并发安全和生命周期错位:goroutine 之间共享全局变量会互相覆盖,而 HTTP handler 启动的 goroutine(比如异步日志、后台任务)可能在 handler 返回后才执行,此时全局变量早已被下一个请求改写。context.Context 就是为解决这个设计的——它自带取消信号 + 不可变键值对 + 明确的生命周期绑定。
context.WithValue 的正确用法和典型误用
它不是用来传业务参数的“万能容器”,而是为框架/中间件传递跨层元数据(如 request_id、user_id、trace_id)。关键约束有三个:
- key 必须是自定义类型(不能用
string),否则不同包间 key 冲突概率高 —— 推荐定义type ctxKey string,再用ctxKey("user_id") - value 必须是可序列化且无副作用的值(避免传 struct 指针,防止下游意外修改)
- 不要嵌套多层
WithValue构建 context,性能差且调试困难;应在入口处(如 middleware)一次性注入所需字段
示例:
type ctxKey string
const UserIDKey ctxKey = "user_id"
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userID := extractUserID(r)
ctx := context.WithValue(r.Context(), UserIDKey, userID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
从中间件取值时为什么总得到 nil
常见原因不是 key 写错,而是上下文没真正传递下去。尤其注意以下场景:
- 调用第三方库(如
database/sql、http.Client.Do)时,必须显式把 context 传进去,否则它们用的是context.Background() - 启动新 goroutine 时,不能直接捕获外层
ctx变量,要传参:go func(ctx context.Context) { ... }(r.Context()),否则拿到的是旧 context 或 nil - 使用
log包默认不读 context,需自己封装 logger,从ctx.Value()提取request_id注入 log 字段
性能影响:WithValue 真的慢吗
单次调用开销约 20–50ns,基本可忽略。但高频滥用会带来两个隐性成本:
立即学习“go语言免费学习笔记(深入)”;
- 每次
WithValue都创建新 context 实例,如果在 for 循环里反复调用,GC 压力明显上升 - 深层嵌套(>10 层)时,
Value查找需遍历链表,O(n) 时间复杂度 —— 这说明设计上应扁平化注入(一个 middleware 注入所有必要字段,而不是每层都加一个) - 更严重的问题是语义污染:把本该由函数参数明确定义的数据藏进 context,会让调用链难以测试和追踪
真正该警惕的不是性能,而是“什么时候不该用 context”——比如 handler 里计算订单总价,就该把 userID 作为参数传给 CalculateTotal(userID, items),而不是让它自己从 context 里取。


















