goroutine泄漏是go服务内存持续上涨的主因,表现为协程卡在select/chan/time.sleep等阻塞点不退出,需监听ctx.done()、正确关闭channel、配对stop timer/ticker,并通过goleak或numgoroutine测试左移拦截。

goroutine 泄漏不是内存泄漏,但会导致内存涨得停不下来
Go 里真正的“内存泄漏”极少,绝大多数线上服务内存持续上涨,根源是 goroutine 泄漏——协程卡在 select、chan receive、time.Sleep 上,既不退出也不释放栈和闭包引用的对象。它不报错,但 runtime.NumGoroutine() 只增不减,pprof 看到几百个状态为 chan receive 的堆栈,基本就是它了。
所有长生命周期 goroutine 必须监听 ctx.Done()
没传 context.Context,或传了但没在阻塞点监听 ctx.Done(),等于给 goroutine 发了单程车票。常见错误包括:闭包捕获外层已 cancel 的 ctx、在 for 循环里只写 time.Sleep 不检查上下文、调用第三方库时用了 Query 而不是 QueryContext。
- 启动 goroutine 时,显式把
ctx作为第一个参数传入,别靠闭包捕获 - 任何可能阻塞的操作(
ch 、<code>、<code>http.Do、db.QueryRow)都要放进select,并加case -
cancel()必须在启动 goroutine 的作用域里defer或明确调用;漏掉这句,等于没加 - timer/ticker 必须配对
Stop(),且不能只靠defer——panic 时它不执行,建议在select退出分支里手动ticker.Stop()
channel 操作必须有兜底,关通道的责任要清晰
向无缓冲 channel 发数据却没人收、从永不关闭的 channel 读数据、for range ch 却没人 close(ch),三者都会让 goroutine 永久挂起。最隐蔽的是 sender 因 panic 或逻辑错误提前退出,忘了 close(ch),receiver 就永远等不到 EOF。
- 谁创建 channel,谁负责
close();receiver 绝对不要 close - 发送方优先用
select+default或超时:case 防止死锁 - 接收方别依赖
for range ch自动退出,改用for { select { case v, ok := - 避免用
len(ch) == 0判断可读性——不可靠,且无法解决阻塞问题
测试阶段就要卡住泄漏,别等上线才看 pprof
靠线上报警发现泄漏太晚。单元测试里就能拦截:用 goleak.VerifyNone 或 runtime.NumGoroutine() 做前后对比,是最轻量也最有效的左移手段。
- 在
TestMain里加goleak.VerifyNone(m),它会自动过滤系统 goroutine,并打出泄漏 goroutine 的初始调用栈 - 自己测
NumGoroutine时,别只采样一次——sleep 后多采几次取最小值,避开 GC/timer 波动干扰 - HTTP handler 测试务必调用
server.Close()或用httptest.NewUnstartedServer控制生命周期 - 别在测试里写
time.Sleep(100 * time.Millisecond)硬等,改用context.WithTimeout+sync.WaitGroup显式等待完成
真正难防的不是技术点,而是“我以为它会自己结束”——比如 defer 里启动 goroutine、方法里隐式持有 struct 全量、日志中间件里拼 map 后忘了清理。这些地方没有标准模式,只能靠 pprof heap + goroutine profile 交叉比对,盯住 runtime.gopark 和闭包符号。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











