最隐蔽的goroutine泄漏点是调用cancel()后goroutine未监听ctx.Done(),导致其卡在I/O或死循环中持续持有资源;必须用select显式监听ctx.Done()并清理。
context.WithCancel/WithTimeout 启动 goroutine 后没监听 Done()
这是最隐蔽也最常被误判的泄漏点:你调用了 cancel(),但 goroutine 里根本没检查 ctx.done(),等于通知发出去了没人听。结果就是 goroutine 卡在 i/o、channel receive 或死循环里,持续持有闭包变量、buffer、http response body 等资源。
常见错误现象:pprof -goroutine 显示大量状态为 chan receive 或 select 的 goroutine,且数量随请求线性增长;pprof -inuse_space 中能看到这些 goroutine 创建的结构体(如 *bytes.Buffer、*http.Request)长期驻留。
- 必须在 goroutine 内部用
select显式监听ctx.Done(),不能只靠 defer cancel() - 如果 goroutine 内部有阻塞调用(如
http.Client.Do),要确保传入的是带超时的ctx,而不是仅靠外部 cancel - 避免在闭包中捕获大对象(如整个 struct、大 slice)——哪怕 goroutine 很快退出,只要 ctx 还活着,这些变量就无法回收
context.Background() 被长期持有导致子 context 泄漏
context.Background() 本身不占内存,但它作为根节点,一旦被某个长期存活对象(如全局 map、sync.Pool、HTTP handler 闭包)意外引用,所有通过它派生的子 context 都会跟着活下来。
使用场景:你在中间件里用 context.WithValue(r.Context(), key, hugeStruct),然后把 r(或其衍生 context)存进了全局缓存,后续再也没清理。
- 不要把 context 存进任何长期生命周期的结构体(尤其是全局变量、单例、cache)
-
context.WithValue只适合传递请求级元数据(如 user ID、trace ID),绝不传业务实体或大对象 - 用
go tool pprof -inuse_space查到可疑结构体后,点「Focus」再看调用栈,重点盯context.WithCancel/context.WithTimeout上游是否来自 handler 闭包或全局初始化逻辑
timer/ticker 绑定 context 后未 Stop() 导致闭包无法释放
time.Timer 和 time.Ticker 在启动时如果绑定了 context(比如用 time.AfterFunc 或自己封装超时逻辑),它们内部会持有该 context 的引用。若忘记调用 Stop(),即使 context 已 cancel,runtime 仍会保留 timer 的 goroutine 和闭包里的所有捕获变量。
错误现象:pprof -goroutine 中出现大量 runtime.timerproc,或 pprof -inuse_space 里看到自定义结构体(如 *handler.State)和 time.Timer 共现于同一调用栈。
- 每个
time.NewTimer/time.NewTicker必须配对Stop(),推荐用defer timer.Stop(),但注意别在循环里 defer(会堆积) - 避免在 closure 中直接捕获大对象后传给
time.AfterFunc——改用显式参数传递,或提前解构 - 用
go tool pprof -http=:8080打开 Web UI,点「View → Top」再过滤time.,能快速定位未 Stop 的 timer 实例
HTTP handler 里用 goroutine + context.WithCancel 但没处理连接关闭
HTTP 客户端断开(如浏览器关闭、移动端网络中断)时,req.Context().Done() 会关闭,但如果你在 handler 里启了一个 goroutine 去做异步任务,并用 context.WithCancel(req.Context()) 派生新 context,却没在 goroutine 里响应 Done(),这个 goroutine 就会继续跑完,甚至把结果写回已关闭的 connection,造成 panic 或资源滞留。
更糟的是:某些反向代理(如 Nginx)可能不会立即通知后端连接断开,导致 goroutine 悬停几十秒甚至几分钟。
- 所有 handler 内启动的 goroutine,都必须监听
req.Context().Done()并做 cleanup(如 close channel、Stop timer、cancel downstream ctx) - 不要依赖
http.CloseNotifier(已弃用),一律用ctx.Done() - 压测时用
curl --max-time 1或 wrk 设置短超时,配合pprof -goroutine?debug=2观察 goroutine 数量是否回落
真正难排查的不是 “有没有 leak”,而是 “为什么明明 cancel 了,对象还在堆上”。关键不在 context 本身,而在谁持有它、谁没响应它、以及它闭包里拖着什么——这些细节在 pprof 调用栈里往往只藏在第三层或第四层。











