必须从入口开始传递context,因其核心是取消传播与超时控制;全局变量无法隔离请求生命周期,手动传参易漏传导致goroutine泄漏;HTTP中应使用req.Context()并用私有类型键调用WithValue。

为什么不能直接用全局变量或函数参数传 context
因为 context.Context 的核心价值是**取消传播**和**超时控制**,全局变量无法隔离请求生命周期,而手动在每个函数参数里加 ctx 又容易漏传、错传——一旦中间某层没往下传,整条链路就断了,上游的 CancelFunc 或超时就失效。
典型错误现象:context deadline exceeded 没触发,或者 ctx.Done() 一直不关闭,goroutine 泄漏。
- 必须从入口(如 HTTP handler、GRPC method)开始带入
ctx - 所有同步调用链上的函数都应把
ctx作为第一个参数 - 启动 goroutine 时,若需继承取消语义,必须用
ctx派生新 context(如context.WithCancel),不能直接闭包捕获外层ctx变量
HTTP handler 中如何正确注入和传递 context
标准库 http.ServeMux 和 net/http 默认只给 handler 传 *http.Request 和 http.ResponseWriter,但 *http.Request 自带 Context() 方法,它已经绑定了连接生命周期、超时、取消等信号。
实操建议:
- 不要自己 new context,优先用
req.Context() - 需要携带额外值(如用户 ID、traceID)时,用
context.WithValue,但注意键类型必须是 unexported 类型,避免冲突 - 下游调用(如 DB 查询、RPC)务必把
ctx传下去,例如db.QueryRowContext(ctx, ...)、client.Do(req.WithContext(ctx))
示例:
func myHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
userID := r.URL.Query().Get("user_id")
ctx = context.WithValue(ctx, userKey{}, userID) // userKey 是私有 struct
<pre class="brush:php;toolbar:false;">if err := doSomething(ctx); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}}
goroutine 启动时 context 传递的常见陷阱
直接在 goroutine 里用外层 ctx 变量看似可行,但若外层函数返回后 ctx 被释放(比如是 WithTimeout 派生的),goroutine 仍持有引用,可能导致 panic 或行为不可控。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
正确做法是:在 goroutine 启动前,用 ctx 派生一个子 context,并在 goroutine 内部监听其 Done()。
- 别写
go func() { /* use ctx */ }() - 要写
go func(ctx context.Context) { /* use ctx */ }(ctx)或更稳妥地派生:childCtx, cancel := context.WithCancel(ctx); defer cancel(); go worker(childCtx) - 如果 goroutine 需独立超时(比如后台清理),用
context.WithTimeout单独控制,不要复用请求级ctx
WithValue 的键类型为什么必须是 unexported
因为 context.WithValue 内部用 == 比较键,如果用 string 或 int 这类公共类型作键,不同包可能无意中覆盖彼此的值,导致取值错乱、调试困难。
实操写法:
- 定义私有类型:
type userKey struct{},然后用context.WithValue(ctx, userKey{}, "123") - 避免用
const key = "user_id"直接当键 - 值尽量轻量,不要塞大结构体或指针——context 是传递控制信号的,不是数据搬运工
取值时也用相同类型:userID, ok := ctx.Value(userKey{}).(string)
实际项目中最容易被忽略的,是中间件或工具函数对 ctx 的“静默截断”:比如某个日志 wrapper 把 ctx 忘了传给下一层,或者封装了 database/sql 但调用的是无 context 版本的 QueryRow。这类问题不会编译报错,但会让整条链路失去超时控制能力。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










