Context 应仅承载轻量元数据或控制信号,禁用裸字符串/整数作键,须用私有结构体封装存取;禁止存业务实体,只存上下文身份;HTTP中间件需显式调用r.WithContext()透传;ctx.Done()后须手动清理资源。

Context 不是万能的数据包,也不是请求参数的快捷通道;它只该承载请求生命周期内必须共享、且无法通过函数参数更清晰表达的轻量元数据或控制信号。滥用 context.WithValue 是 Go 服务内存泄漏和隐式耦合的头号推手。
为什么不能用 string 或 int 当 Context 键
用 "user_id" 或 1 这类裸值作键,等于主动放弃类型安全和语义隔离:
- 拼错键名(比如
"user_id"vs"userid")时,ctx.Value()静默返回nil,编译器完全不报错 - 不同模块用相同字符串键写入不同类型(
int64和*User),下游断言会 panic - 无法 grep 到所有使用位置——因为键可能被赋给变量再传入,或拼接生成
正确做法是定义私有空结构体:type userIDKey struct{},再封装存取函数:func WithUserID(ctx context.Context, id int64) context.Context 和 func UserIDFrom(ctx context.Context) (int64, bool)。这样既锁死键类型,又把断言逻辑收口到一处。
哪些数据能进 Context,哪些必须走函数参数
Context 只接受“上下文身份”,不接受“业务实体”:
立即学习“go语言免费学习笔记(深入)”;
- ✅ 允许存:请求 ID(
string)、用户 ID(int64)、租户标识(uuid.UUID)、traceID(string)、是否调试模式(bool) - ❌ 禁止存:
*Order、map[string]interface{}、JSON 字节切片、带方法的结构体指针
原因很实在:大对象长期滞留于请求生命周期的 Context 树中,GC 无法及时回收;更关键的是,下游函数靠 ctx.Value() 拿业务数据,等于绕过函数签名,调用链不可见、单元测试难构造、重构成本高。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
HTTP 中间件里 Context 透传的致命漏点
http.Handler 接口不带 context.Context 参数,所以中间件必须手动从 *http.Request 提取并注入新 Context,再交给下一层 handler —— 但极易漏掉关键一步:重赋值 r = r.WithContext(newCtx)。
错误写法(下游拿不到新 ctx):
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
newCtx := context.WithValue(r.Context(), userKey{}, u)
// ❌ 忘记把 newCtx 写回 request
next.ServeHTTP(w, r) // r.Context() 还是原来的!
})
}
正确写法:
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
newCtx := context.WithValue(r.Context(), userKey{}, u)
r = r.WithContext(newCtx) // ✅ 必须这行
next.ServeHTTP(w, r)
})
}
取消与超时之后的资源清理常被忽略
ctx.Done() 关闭只代表“该停了”,不代表“已停完”。常见疏漏:
- 监听
ctx.Done()后直接 return,没关闭打开的文件句柄、数据库连接或 channel - 在 goroutine 中启动子 goroutine 但没传递 ctx,导致父 ctx 取消后子 goroutine 继续跑
- 用
context.WithTimeout但没检查ctx.Err()就继续操作,结果触发context.DeadlineExceeded后的 panic
真正安全的做法是:在 select 响应 ctx.Done() 后,显式释放资源(close(ch)、conn.Close()、mu.Unlock()),再 return。不要假设“通道关了,事情就结束了”。

















