调用cancel()不等于goroutine自动退出,它仅关闭ctx.Done()通道;goroutine必须显式select监听该通道并响应,否则持续运行导致资源泄漏。

context.WithCancel/WithTimeout 启动 goroutine 后没监听 ctx.Done()
调用 cancel() 不等于 goroutine 自动退出——它只是往 ctx.Done() 发个信号,没人监听就彻底失效。最常踩的坑是:在 HTTP handler 里启了个 goroutine 做异步处理,传了带超时的 ctx,但 goroutine 内部既没 select 监听 ctx.Done(),也没把 ctx 传给 http.Client.Do() 这类阻塞调用。
常见错误现象:pprof -goroutine 显示大量状态为 chan receive 或 select 的 goroutine,数量随请求线性增长;pprof -inuse_space 里能看到 *bytes.Buffer、*http.Response 等对象长期驻留。
- 必须在 goroutine 内部用
select显式监听ctx.Done(),并在分支里做清理(如关闭 channel、释放 buffer) - 如果调用了
http.Client.Do(req.WithContext(ctx)),仍要确保resp.Body.Close()在ctx.Done()触发后能执行,否则 body 缓冲区卡住 - 别依赖
defer cancel()包裹整个 long-running loop——函数不返回,defer就永不执行
把 context 存进全局结构体或缓存导致子节点全泄漏
context.Background() 本身不占内存,但它一旦被长期存活对象意外持有(比如存进全局 sync.Map、单例 service 字段、HTTP 中间件闭包),所有从它派生的子 context 都跟着活下来,连带它们 WithValue 里的大对象也永远无法回收。
使用场景:你在中间件里写 ctx = context.WithValue(r.Context(), userKey, hugeUserStruct),然后把 r 或这个 ctx 存进了某个全局 map,后续再也没删。
立即学习“go语言免费学习笔记(深入)”;
- 绝不在任何生命周期超过单次请求的结构体中保存
context或其派生值 -
context.WithValue只适合传轻量标识:字符串型traceID、整数型userID,别传指针、结构体、切片 - 避免嵌套调用
WithValue:一次注入必要字段即可,不要在每层 middleware 都套一层
timer/ticker 绑定 context 后未 Stop() 导致闭包变量钉死堆上
context.WithTimeout 内部启动了一个 time.Timer,它会强引用整个闭包环境。如果没显式调用返回的 cancel(),或者调用了但 timer 没 Stop(),那 timer goroutine 和它捕获的所有变量(比如 handler 里的 *DB、config)就一直钉在堆上。
错误现象:pprof -goroutine 出现大量 runtime.timerproc;pprof -inuse_space 里 *time.Timer 和你的业务结构体(如 *handler.State)共现于同一调用栈。
- 每次调用
context.WithCancel或context.WithTimeout,都必须有且仅有一个明确的cancel()调用点 - 别把
cancel函数存进结构体字段(如type Service struct { cancel context.CancelFunc }),这会让 Service 生命周期绑架 timer - 如果封装了带 context 的定时逻辑(比如
AfterFunc(ctx, f)),内部必须确保 timer 在ctx.Done()后调用Stop()
pprof 差分抓 heap profile 时容易忽略的关键细节
抓不到真泄漏,往往不是代码问题,而是 profile 采样方式错了。很多开发者看到内存涨就抓 /debug/pprof/heap,结果 profile 里全是临时对象,根本看不到泄漏源。
关键参数差异:-inuse_space 看当前驻留内存(必须用),-alloc_space 看总分配量(对泄漏诊断基本无用);?gc=1 强制 GC 后采样会刷掉长期存活对象,反而掩盖问题。
- 采样前等服务稳定运行 ≥5 分钟,避开启动预热阶段
- 两次采样间隔 ≥30 秒,文件名必须带
.heap或.pb.gz后缀 - 用
go tool pprof -base before.heap after.heap差分,进 Web UI 后切到inuse_space→View → Difference→Focus输入可疑类型(如*model.UserCache) - 重点盯调用栈里是否夹着
context.WithCancel、timerproc、http.HandlerFunc——这些是泄漏链的典型锚点
真正难的不是发现 pprof 里哪个结构体占得多,而是判断它为什么没被回收:是 goroutine 卡住了?context 被谁长期持有了?timer 忘记 stop 了?这些链条一环扣一环,漏掉任意一环,修复就只是表面功夫。


















