StackInuse异常飙升而HeapAlloc平缓增长,表明goroutine栈泄漏或连续栈扩容失控;每秒采集并对比StackInuse与HeapInuse增速可定位问题,配合pprof/goroutine?debug=2抓阻塞点确认chan send/receive、select{}等挂起状态。

看 runtime.ReadMemStats().StackInuse 是否异常飙升
内存暴涨但 HeapAlloc 增长平缓?先盯 StackInuse。每个 goroutine 至少占 2KB 初始栈,若泄漏数万协程,光栈就吃掉上百 MB;更糟的是,深递归或大局部数组会触发连续栈扩容(2KB→4KB→8KB→…),StackInuse 可能从 100MB 瞬间飙到 3GB+,同时 CPU 被 runtime.copystack 占满。
实操建议:
- 每秒采集一次:
var ms runtime.MemStats; runtime.ReadMemStats(&ms); log.Printf("StackInuse: %.2f MB", float64(ms.StackInuse)/1024/1024) - 对比
StackInuse和HeapInuse增速:前者涨得快,基本锁定是 goroutine 栈失控,不是堆对象泄漏 - 别复用同一个
runtime.MemStats变量,否则字段会被覆盖,数据失真
用 pprof/goroutine?debug=2 抓阻塞点
/debug/pprof/goroutine?debug=2 返回的是全量 goroutine 堆栈快照,不是数量统计。重点不是“有多少”,而是“卡在哪”。泄漏协程几乎都停在几个固定状态上:
-
chan send:无缓冲 channel 发送无人接收,或缓冲区满后继续发 -
chan receive:接收方已退出,发送方还在等 -
select {}或空for {}:典型永久挂起 -
semacquire:锁未释放,或sync.WaitGroup.Wait()永不返回
注意:日志可能刷不出来——协程卡住后根本执行不到 log.Print 那行。信 pprof 快照,不信日志埋点。
立即学习“go语言免费学习笔记(深入)”;
别只关 channel,必须传 context.Context
close(ch) 对发送方完全无效,尤其无缓冲 channel。修复的关键是让 goroutine 主动感知退出信号:
- 所有异步启动的 goroutine 函数签名必须带
ctx context.Context - 阻塞操作前加
select监听:case ,不能只依赖 <code>for range ch - worker 循环里不能只靠
range退出,要显式检查if ctx.Err() != nil - HTTP handler 中启 goroutine,务必把 handler 的
ctx传进去,而不是用context.Background()
上线前用 goleak 拦住测试泄漏
单元测试里随手起的 go func() { ... }() 是最隐蔽的生产泄漏源——测试进程退出时,这些 goroutine 还活着,且没人等它们。用 goleak 在测试结束时自动扫描:
- 在
TestMain里加:goleak.VerifyTestMain(m) - 它只报非标准协程:比如你写的
go worker(),但放过http.Server、time.AfterFunc等标准库协程 - 失败时直接打印 goroutine 启动堆栈,精准定位到哪行
go关键字 - 不跑
goleak就合代码,等于放行泄漏进 CI
真正难的不是加 ctx 或 close,而是让每个 goroutine 都有明确的生命周期边界——它什么时候该生,什么时候必须死,这个边界得由调用方定义,不能靠运气等 GC 或 OS 收尸。


















