goroutine泄漏导致内存持续上涨且GC无法回收,因阻塞的goroutine栈不断扩容,表现为StackInuse飙升、RSS缓慢上升、最终OOM;需用pprof定位chan send/receive/select阻塞点,并通过context.Context控制退出。

goroutine 泄漏直接导致内存持续上涨
Go 的 GC 不回收仍在运行的 goroutine,哪怕它只是卡在 chan send 或 select 上。每个泄漏的 goroutine 至少占用 2KB 初始栈,深度阻塞时会不断扩容,runtime.ReadMemStats().StackInuse 字段会明显飙升。你不会看到 panic: stack overflow,只会发现 RSS 内存缓慢爬升、接口延迟抖动、最终被 OOM killer 杀掉。
用 pprof 快速定位泄漏 goroutine 的阻塞点
别只看 goroutine 数量,要抓实际阻塞位置:
- 启动服务时注册:
import _ "net/http/pprof",再起个http.ListenAndServe(":6060", nil) - 压测后执行:
go tool pprof http://localhost:6060/debug/pprof/goroutine?debug=2 - 重点找状态为
chan send、chan receive、select的堆栈,尤其是重复出现的函数名(如fetchAsync、workerLoop) - 配合
go tool pprof http://localhost:6060/debug/pprof/heap,对比StackInuse和HeapInuse增长趋势 —— 若前者涨得更快,基本锁定是 goroutine 栈失控
修复必须带退出信号,不能只靠 close(channel)
关闭 channel 只解决“接收方”退出问题,对“发送方”无用;无缓冲 channel 上的发送操作,若无人接收,close(ch) 也不解阻塞。
- 所有异步 goroutine 必须接收
context.Context参数,并在关键阻塞点用select监听ctx.Done() - 错误示例:
result —— 没有 fallback,一旦接收方退出就永久卡住 - 正确写法:
select { case result - worker 循环必须显式检查
ctx.Err() != nil,不能只依赖for range ch—— channel 关闭后range才退出,但若 channel 永不关闭,循环永不停
测试阶段用 goleak 拦住泄漏代码上线
单元测试里起 goroutine 是最隐蔽的泄漏温床 —— 测试跑完进程还在,泄漏 goroutine 就赖着不走。
立即学习“go语言免费学习笔记(深入)”;
- 在包级
TestMain中接入:goleak.VerifyTestMain(m) - 它会在所有测试结束后扫描存活 goroutine,只要发现非标准协程(比如没被
sync.WaitGroup等待、也没监听ctx.Done()的),立刻失败并打印启动堆栈 - 特别注意:goleak 不会报
time.AfterFunc或http.Server这类标准库协程,但会精准揪出你写的go func() { ... }() - 修复后务必重跑测试 —— goleak 是唯一能提前拦截泄漏进入生产环境的守门员
真正难的不是加 context 或 close channel,而是判断「谁该负责退出」。一个 goroutine 启动后,它的生命周期必须绑定到某个明确的 owner(比如传入的 ctx、某个 WaitGroup、或一个可关闭的 service struct),否则它就是漂浮的资源黑洞。


















