Context只传请求元数据(如traceID、userID),禁传业务字段;Key须用未导出空struct防冲突;HTTP handler必须基于r.Context()派生;应用级依赖应通过闭包或构造函数注入。

Context 不能传业务字段,只传请求元数据
直接往 context.WithValue 里塞 orderID、user.Email 或结构体指针,等于埋雷。这不是设计缺陷,是误用——context.Value 的定位从来不是替代函数参数,而是承载跨层只读元数据(如 traceID、requestID、userID)。一旦传了业务字段,调用方无法从函数签名看出依赖,单元测试得 mock 整个 context,下游想改值还得重建新 context(因为不可变),类型安全也彻底丢失。
Key 必须用未导出空 struct,别用 string
用 "user_id" 当 key 是最常见也最危险的错误。不同包都这么写,ctx.Value("user_id") 可能取到别人塞的值,编译器完全不报错。正确做法是定义私有类型:
type userIDKey struct{}
然后统一用 userIDKey{} 读写:
ctx = context.WithValue(ctx, userIDKey{}, 123)
id, ok := ctx.Value(userIDKey{}).(int64)
好处很实在:IDE 能跳转到定义,拼错类型编译直接报错,不同模块即使名字一样也不会冲突。
立即学习“go语言免费学习笔记(深入)”;
HTTP handler 到子包必须基于 r.Context() 派生
在 handler 里写 ctx := context.Background() 或硬编码超时,等于切断请求生命周期信号。客户端断开连接、超时中断,goroutine 还在跑。正确链路是:
- 从
r.Context()开始派生,比如ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second) - 所有下游调用(DB 查询、HTTP 请求、gRPC)必须显式传这个
ctx,例如db.QueryRowContext(ctx, ...)、client.Do(req.WithContext(ctx)) - 中间件注入值也必须基于
r.Context(),再用r.WithContext(newCtx)更新请求对象,否则下游收不到
闭包比全局变量更适合传应用级上下文
数据库连接池、配置、日志实例这类应用级依赖,不该塞进 context,也不该用全局变量。更安全的做法是闭包封装:
func CreateUserHandler(appCtx *AppContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// ✅ appCtx 来自闭包捕获,线程安全、可测试、无并发风险
user, err := appCtx.DB.Create(r.Context(), ...)
}
}
路由注册时传入当前上下文:r.HandleFunc("/user", CreateUserHandler(appCtx))。这样既避免全局状态污染,又让依赖显式可见,单元测试时直接传 mock 实例即可。
真正容易被忽略的是:context 不是万能胶水,它只管请求生命周期和元数据穿透;业务逻辑需要的参数,必须走函数签名;应用级依赖,应该靠构造函数或闭包注入。混用这三类传递方式,才是多数 context 问题的根源。


















