应从r.context()获取请求上下文并向下传递,中间件需用req.withcontext()更新request,仅对异步goroutine派生带超时/取消的ctx,业务参数须显式传参而非context.withvalue。

Go HTTP Handler 中如何拿到并传递 context.Context
标准 net/http 的 Handler 接口不接收 context.Context 参数,所以不能直接“传入”。必须从 http.Request 里显式提取——因为 Go 1.7+ 起,每个 *http.Request 都自带一个绑定的 Context() 方法。
常见错误是手动新建 context.Background() 或硬塞 context.TODO(),这会导致超时、取消信号丢失,上游请求中断后下游 goroutine 仍在运行。
- 正确做法:在 handler 入口调用
r.Context()获取请求上下文,再向下传递 - 不要覆盖
r.Context()返回的 ctx,除非你明确需要派生新上下文(如加超时、加值) - 若需添加请求 ID 或用户信息,用
context.WithValue(),但 key 必须是自定义类型(避免字符串冲突),例如type ctxKey string; const userIDKey ctxKey = "user_id"
中间件中如何安全地增强 context 并避免泄漏
中间件本质是包装 http.Handler,它必须把增强后的 context 注入到 request 中,才能让下游 handler 拿到。Go 不允许直接修改 http.Request.Context() 返回的 ctx,但提供了 req.WithContext() 方法来生成新 request 实例。
典型陷阱是:只改了局部变量 ctx 却没更新 req,导致下游仍拿到原始上下文。
- 必须用
req = req.WithContext(newCtx)替换 request 对象 - 中间件链中每层都应调用
next.ServeHTTP(w, req),而非原 request - 若中间件做了鉴权并附加用户信息,
context.WithValue()存的值仅在当前请求生命周期有效,无需担心并发写冲突(因为 ctx 是只读的)
WithCancel / WithTimeout 在 handler 中何时该用、何时不该用
不是所有 handler 都需要自己创建可取消上下文。多数情况应复用 r.Context();只有当你启动了独立于请求生命周期的 goroutine(比如异步日志上报、后台任务触发),才需要派生新上下文并管理其生命周期。
滥用 WithCancel 会导致 cancel 函数被意外调用,提前终止本该继续的请求流程;滥用 WithTimeout 则可能覆盖客户端本身设置的 deadline(比如反向代理已设了 30s 超时)。
- 推荐模式:
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second),然后defer cancel() - 慎用
WithCancel:仅当你要主动控制子任务(如等待多个 RPC 返回,任一失败就终止其余) - 绝对不要在 handler 外部调用
cancel(),否则可能中断其他正在使用的同一 ctx 的 goroutine
Value() 传参的边界与性能隐患
context.WithValue() 看似方便,但它底层是链表查找,深度越深、key 越多,ctx.Value(key) 越慢。更严重的是,它破坏了函数签名的可读性——调用方无法从函数声明看出依赖哪些上下文数据。
很多团队踩过坑:把业务参数(如订单 ID、分页 size)全塞进 context,结果调试时找不到来源,单元测试难 mock,重构时不敢动。
- 只传元数据:trace ID、user ID、request ID、tenant ID 这类“请求身份标识”
- 业务参数必须走函数参数显式传递,哪怕多写两个参数
- 如果真要共享结构体,优先考虑封装成中间件注入的 struct 字段(如
c.User),而不是ctx.Value(userKey)
真正难的不是怎么用 WithValue 或 WithTimeout,而是判断某个数据该不该进 context、某个 goroutine 该不该受当前请求 ctx 控制。这些决策一旦错位,问题会藏得很深——比如一个后台推送协程意外继承了 HTTP 请求 ctx,导致请求结束就静默退出,推送永远发不出去。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











