req.Context() 由 HTTP 服务器在请求接收时自动创建并绑定生命周期,请求结束(超时、取消或响应完成)时自动取消;禁止缓存该 Context 到全局变量、结构体字段或 map 中,必须在 handler 内每次调用 r.Context() 并立即传入下游函数。

Context 在 net/http.Request 中从哪来
Go 1.7+ 的 net/http.Request 自带 Context() 方法,返回的 context.Context 是由 HTTP 服务器在接收请求时自动创建的。它不是你手动传入的,而是内置生命周期绑定:请求开始时派生,请求结束(超时、取消、响应写完)时自动取消。
这意味着你无需自己调用 context.WithCancel 或 context.WithTimeout 来管理请求生命周期——只要用 req.Context() 就行,它已经和连接、超时、客户端断开等事件联动好了。
为什么不能在 handler 外部保存 req.Context() 的值
常见错误是把 req.Context() 存成包级变量、全局 map 或结构体字段,后续异步 goroutine 里再用。这会导致 context 被意外复用或泄漏,尤其当请求已结束但 goroutine 还在运行时,ctx.Err() 可能已为 context.Canceled,但代码没检查就继续执行。
正确做法始终在 handler 内部按需获取,并立即传递给下游函数:
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() // 每次都重新取,不缓存
result, err := doSomething(ctx)
// ...
}
- 不要在
http.HandlerFunc外定义var globalCtx context.Context - 不要把
r.Context()存进 struct 的字段里供后续方法调用 - 如果要启动 goroutine,必须显式传入
ctx,并用select { case 响应取消
如何安全地向 Context 添加自定义值
用 context.WithValue 可以塞入请求级元数据(如用户 ID、trace ID),但必须遵守两个硬约束:
- key 类型必须是 unexported 类型(避免第三方包冲突),推荐用私有 struct 或指针
- 只放只读、不可变的小数据,别传大对象或接口实现
示例:
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) {
uid := extractUserID(r)
ctx := context.WithValue(r.Context(), userIDKey, uid)
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}
// 使用时:
func handler(w http.ResponseWriter, r *http.Request) {
if uid, ok := r.Context().Value(userIDKey).(string); ok {
// 安全取值
}
}
注意:context.WithValue 不是替代参数传递的捷径。如果函数逻辑明确依赖某个值,优先作为参数传入;仅当跨多层中间件且无法修改签名时才用。
Context 超时与 cancel 的实际表现
HTTP server 的 ReadTimeout、WriteTimeout、IdleTimeout 都会触发 req.Context().Done()。但关键点是:这些超时并不直接 kill goroutine,只是让 ctx.Err() 变为非 nil —— 你得主动检查。
典型遗漏场景:
- 数据库查询未传
ctx,导致超时后还在等 DB 响应 - 调用
http.Client.Do(req)时没把req.Context()注入到 outbound request - 循环读 body(如流式解析 JSON)没在每次
Read()后检查ctx.Err()
务必在阻塞操作前确认上下文状态:
http.ResponseWriter











