WaitGroup 本身不导致内存泄漏,但会掩盖 goroutine 泄漏:Add/Done 不匹配或 goroutine 内部阻塞,使 wg.Wait() 永不返回,导致 goroutine 及其栈内存持续累积,最终引发 OOM。
waitgroup 不是内存泄漏的直接原因,但它会掩盖 goroutine 泄漏,让泄漏更难被发现、更难被回收——最终表现为内存持续上涨、oom 崩溃。
WaitGroup.Add 和 Done 调用不匹配
这是最常见也最隐蔽的问题:Add(1) 了,但某个 goroutine 因 panic、提前 return 或逻辑跳过,没走到 Done()。结果 wg.Wait() 永远阻塞,所有已启动的 goroutine 都卡在那儿不动,栈内存(2KB 起)持续累积。
- 典型错误写法:
go func() { defer wg.Done(); doWork() }()—— 如果doWork()panic,defer wg.Done()根本不会执行 - 正确做法:把 recover 和 Done 绑定在同一个 defer 里:
defer func() { if r := recover(); r != nil { /* log */ } wg.Done() }() - 更稳妥方式:改用
errgroup.Group,它自动处理 panic + Done + 上下文取消,不用手写
goroutine 内部阻塞导致 WaitGroup 无法退出
WaitGroup 只管“计数归零”,不管 goroutine 是否还活着。如果 goroutine 卡在 、<code>select {}、time.Sleep 或网络等待上,Done() 永远不会被调用,wg.Wait() 就永远等下去。
AI 舌诊面诊问诊医疗系统 + 诊疗商城系统源码是一套结合 AI 辅助诊断与商品转化的健康类程序,基于浩创云、ThinkPHP(PHP)开发,支持微信服务号与 H5 端,适合做中医舌诊面诊问答、健康建议推送以及配套诊疗商品的运营。
- 例如:
go func() { wg.Add(1); —— 这个 goroutine 启动即永久阻塞,<code>wg.Add(1)还在函数体外,根本没进 goroutine,更别说Done() - 修复关键:所有可能阻塞的操作,必须包裹在
select中监听ctx.Done(),并在收到时主动 return - 别依赖
runtime.GC()—— 它对卡住的 goroutine 无效,栈内存不会被回收
WaitGroup 被跨 goroutine 复用或误共享
WaitGroup 不是线程安全的“可复制对象”。把它作为值传递(比如方法接收者是 WaitGroup 而非 *WaitGroup),会导致每个 goroutine 操作的是自己的副本,wg.Wait() 永远等不到真实计数归零。
- 错误示例:
func (wg WaitGroup) Do() { wg.Add(1); go func() { wg.Done() }() }——wg是拷贝,Add和Done影响的是不同实例 - 必须传指针:
func (wg *WaitGroup) Do() { wg.Add(1); go func() { wg.Done() }() } - 更危险的是:多个 goroutine 同时调用
wg.Add(n)而没加锁 ——Add本身不是原子操作,可能导致计数错乱
真正难排查的,从来不是 wg.Wait() 卡住,而是那些“看起来执行完了”的 goroutine 其实卡在 select 或 channel 上,还在偷偷吃内存。pprof 查 /debug/pprof/goroutine?debug=2 时,重点盯 chan receive、semacquire、select 这几类堆栈,比盯着 WaitGroup 本身有用得多。

















