必须调用cancel函数,否则context.WithCancel/WithTimeout返回的子context无法释放底层timer和goroutine,导致泄漏;HTTP handler中漏defer cancel()会使后台goroutine在请求结束后持续阻塞。

为什么 context.WithCancel 返回的 cancel 必须调用
不调用 cancel() 会导致 goroutine 泄漏和内存持续占用,尤其在 HTTP handler 或定时任务中反复创建子 context 时。Go 的 context 不会自动清理底层 channel 和 timer,靠手动调用 cancel 触发关闭逻辑。
常见错误现象:net/http handler 中用 context.WithTimeout 启动后台 goroutine,但没 defer cancel(),请求结束后 goroutine 仍在运行,pprof/goroutine 显示大量阻塞在 select 或 time.Sleep。
- 只要调用了
context.WithCancel/WithTimeout/WithDeadline,就默认你承担清理责任 - HTTP handler 中推荐写法:
func handler(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second) defer cancel() // 关键:必须 defer,不能只在 success 分支里调用 // ... 使用 ctx 做数据库查询、RPC 调用等 } - 如果子 goroutine 需要长期持有 context(如后台轮询),且父 context 可能提前结束,应显式复制 value 或用
context.Background()重新派生,而非直接传递被 cancel 的 ctx
如何安全地往 context 里塞自定义值
context.WithValue 不是通用状态传递机制,它只适合传递“请求范围的、不可变的元数据”,比如用户 ID、trace ID、tenant key。滥用会导致类型混乱、难以调试、性能下降(每次 Value 查找都是链表遍历)。
使用场景受限:仅当跨多层函数调用(如 middleware → service → repo)又无法改参数签名时才用;否则优先通过函数参数显式传入。
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- key 类型必须是 unexported 类型(避免第三方包冲突),推荐用私有 struct 或
type ctxKey string+ 全局变量 - 不要传指针或大结构体——
context生命周期可能很长,易引发内存泄漏 - 示例正确写法:
type userCtxKey struct{} func WithUser(ctx context.Context, u *User) context.Context { return context.WithValue(ctx, userCtxKey{}, u) } func UserFromCtx(ctx context.Context) (*User, bool) { u, ok := ctx.Value(userCtxKey{}).(*User) return u, ok }
为什么 http.Request.Context() 在中间件里不能直接传给下游
很多开发者在 Gin/echo 中间件里直接把 r.Context() 传给 service 层,看似省事,实则埋下超时与取消失控的隐患。因为 http.Request.Context() 的生命周期绑定到整个 HTTP 连接,而业务逻辑往往需要更细粒度的控制(如 DB 查询超时 2s,外部 API 调用超时 3s)。
真正该做的是:在每个具体操作前,基于原始 request context 派生新的子 context,并设独立 deadline/cancel。
- 错误:
service.DoSomething(r.Context())—— 一旦客户端断连,整个业务流程立即中断,哪怕 DB 查询已发出也无法等待结果 - 正确:
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second); defer cancel(); service.DoSomething(ctx) - 若需保留 traceID 等值,先
WithValue再WithTimeout,顺序不能反(value 会被 cancel context 继承,反之不行)
goroutine 启动后忘记 select ctx.Done() 会怎样
启动 goroutine 时若未监听 ctx.Done(),该 goroutine 就脱离 context 控制,变成“孤儿”。即使父 context 已 cancel,它仍继续运行,直到自然结束或 panic。
典型表现:日志里看不到预期的 “context canceled” 错误,但 CPU/内存缓慢上涨;runtime.NumGoroutine() 持续增加。
- 所有阻塞操作(
time.Sleep、ch 、<code>db.QueryRowContext、http.Do)都必须配合ctx.Done()做 select 判断 - 不要写
for { ... time.Sleep(1 * time.Second) },而要:for { select { case <-time.After(1 * time.Second): // do work case <-ctx.Done(): return // 退出 goroutine } } - 第三方库(如
database/sql、net/http)虽支持 context,但只保证“发起时检查”,不保证“执行中响应 cancel”——某些驱动或底层连接可能忽略,所以业务层仍需主动判断ctx.Err()
实际项目中最容易被忽略的,是 cancel 函数的调用时机和 goroutine 内部对 ctx.Done() 的响应完整性。这两点不落实,context 就只是个摆设。

















