不能直接用 context.Background() 处理 HTTP 请求,因为它无生命周期绑定,会导致 goroutine 泄漏、超时失效、传值断裂;必须基于 http.Request.Context() 衍生带取消能力的 context。

为什么不能直接用 context.Background() 处理 HTTP 请求
因为 context.Background() 没有生命周期绑定,无法随请求结束自动取消,会导致 goroutine 泄漏、超时控制失效、中间件传值断裂。真实 Web 请求必须用 context.WithCancel() 或 context.WithTimeout() 基于 http.Request.Context() 衍生。
Go 的 http.Server 在每次请求开始时自动创建一个带取消能力的 context,并通过 req.Context() 暴露——这是唯一安全的起点。
- 别手动 new context 传给 handler,它和请求生命周期脱钩
- 别在中间件里用
context.WithValue()往context.Background()塞数据,值会丢失 - 所有下游调用(DB 查询、HTTP 调用、定时任务)都应接收并传递该 request-scoped context
如何安全地往请求 context 里塞自定义数据
用 context.WithValue() 是可行的,但必须满足两个条件:key 类型是导出的未比较类型(避免冲突),且值是只读的(不修改原始 context)。
常见错误是用字符串或 int 当 key,导致不同包之间 key 冲突;或者把 map、slice 这类可变对象塞进去,被下游意外修改。
- 定义 key:
type ctxKey string; const UserIDKey ctxKey = "user_id" - 存值:
ctx = context.WithValue(r.Context(), UserIDKey, userID) - 取值:
if userID, ok := ctx.Value(UserIDKey).(int64); ok { ... } - 绝不传指针或可变结构体;如需复杂结构,用
struct{}包一层并设为 unexported 字段
http.Request.WithContext() 和中间件中替换 context 的区别
req.WithContext() 返回新 *http.Request,但原 request 不变;而中间件链中「传递」context 的本质,是把新 context 注入到下一个 handler 的 http.Handler 调用中——不是改 req,而是换掉传给下一级的 context。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
典型中间件写法:
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// 鉴权成功后注入 user info
ctx = context.WithValue(ctx, UserKey, user)
// 注意:这里没改 r,而是用新 ctx 构造新 *http.Request
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}
- 必须用
r.WithContext()才能让下游 handler 通过r.Context()拿到新值 - 如果只是
ctx = context.WithValue(r.Context(), ...)但没调r.WithContext(),下游仍看到旧 context -
WithContext()开销极小,是浅拷贝,放心用
超时与取消传播失败的三个高频原因
即使用了 req.Context(),下游依然不响应 cancel,往往卡在底层阻塞调用没接 context。这不是 context 本身的问题,而是调用链断了。
- DB 查询没传
ctx:比如db.Query("...")→ 应该是db.QueryContext(ctx, "...") - 第三方 HTTP 客户端没透传:用
http.DefaultClient.Do(req)→ 改成http.NewRequestWithContext(ctx, ...)再client.Do() - 自定义 channel select 没监听
ctx.Done():比如select { case → 必须加case
真正难调试的是「看起来用了 context,但某一层忘了透传」——建议所有接受 context 的函数签名都显式带 ctx context.Context 参数,不默认 fallback 到 context.Background()。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










