CClara轻量论坛系统

Go协程泄漏的三种现场与排查思路

陈先生
陈先生 正式会员正式会员认证极客认证极客
发布于 2026-08-09 04:04 ·152 浏览 ·0 回复
内容摘要

Go 协程泄漏指协程启动后无法退出,持续占用栈空间和堆引用,导致线上服务内存上涨、响应变慢,pprof 中 goroutine 数量激增时需重点排查;常见现场是 channel 发送与接收双方因缺少超时或取消机制而永久阻塞,应结合超时、取消和 pprof 定位未退出协程。

Go 协程轻量,但轻量不代表可以随意放任。线上服务跑着跑着内存涨上去、响应变慢,pprof 一抓,goroutine 数量成千上万,多半就是协程泄漏了。所谓泄漏,就是协程启动了却永远无法退出,占着栈空间和堆引用,逐渐把资源耗干。这篇文章整理三种最常见的泄漏现场,以及对应的排查路径。

现场一:channel 发送/接收双方永远等不到对方

这是最经典的泄漏场景。一个协程往 channel 里发数据,但接收者已经退出;或者一个协程等着从 channel 读数据,但发送者永远不来。双方如果没有超时或取消机制,协程就会一直阻塞在那里。

举个例子:

ch := make(chan int)
go func() {
    ch <- 42  // 永远没人接收,goroutine 泄漏
}()
// 主流程继续做别的事,没有接收 ch

更隐蔽的是在业务循环中,每个请求都启动一个协程等待某个 channel,而该 channel 的写入逻辑因为提前 return 或 panic 被跳过了。

排查思路:用 pprof 抓 goroutine,在 runtime/pprof 的输出里找到阻塞在 chan send 或 chan receive 的栈。重点看调用栈的上下文,确认对端协程是否已经不存在。修复手段就是加缓冲、加超时、或者用 context 取消来兜底。推荐使用 select 配合 time.After 或 ctx.Done(),让协程有路可退。

现场二:sync.WaitGroup 计数未归零导致永久等待

不少同学喜欢用 WaitGroup 等一批协程结束,但如果在执行协程前 Add 次数不够,或者协程内部因阻塞没执行 Done,主流程就会一直卡在 Wait() 上。虽然这种不算标准意义上的“协程泄漏”,但同样会造成协程堆积——如果主流程是每来一个请求就起一组协程并 Wait,前一组没结束,后面的请求继续堆积,goroutine 数量自然会飙升。

常见错误是子协程里又起了子协程,却把 WaitGroup 的 Done 放在了外层协程的最后,而外层协程可能因为内层还没完成就提前 Done,导致内层的 Wait 永远等不到。或者是在循环里误用了 go 关键字,把 Add 放在 goroutine 内部执行,结果主流程 Wait 时计数还没加上。

排查思路:抓 goroutine 栈时看到阻塞在 sync.WaitGroup.Wait 的协程,顺着调用栈找到创建 WaitGroup 的位置。检查所有 Done 的分支是否覆盖了提前 return、panic 等情况。更稳妥的做法是在协程入口用 defer wg.Done(),并且确保 Add 在启动协程之前完成。

现场三:锁未释放或 time.Ticker 忘记停止

互斥锁(sync.Mutex)如果加锁后忘了解锁,或者因为某段逻辑 panic 导致 Unlock 没执行,另一个获取锁的协程就会永远阻塞。这虽然不是协程自己启动,但会让持有锁的协程和等待锁的协程一起形成死锁状态,在 pprof 里表现为大量 goroutine 阻塞在 sync.Mutex.Lock。

另外,time.Ticker 也是一个容易忽略的点。如果创建了 Ticker 但从不 Stop,它的 channel 会持续产生事件,关联的协程如果不退出就会一直执行,看起来像活跃泄漏。更常见的是用 for range ticker.C 的协程,外层没有退出条件。

排查思路:pprof 栈上会显示 sync.Mutex.Lock 阻塞,并且能看到当前持锁的 goroutine 是哪个(在 dump 里查找 Mutex 或 Lock 相关的地址)。如果是 panic 导致解锁失败,建议加 defer 来保证解锁。对于 Ticker,凡是 time.NewTicker 的地方都必须有对应的 Stop。可以用 go vet 检查部分问题,但最好的办法是代码审查时建立 checklist。

实战排查建议

以上三种现场,核心工具都是 Go 内置的 pprof。启动服务时引入 net/http/pprof,然后通过 go tool pprof http://localhost:port/debug/pprof/goroutine 获取 dump。用 web 或 tree 视图,找到数量异常大的阻塞栈。

还有一种简单却有效的方式:在测试环境定期输出 goroutine 数量,观察是否有单调递增趋势。如果协程总数只增不减,说明存在泄漏。对于已经泄漏的协程,直接 kill 服务没用,关键是找到泄漏点并修复。建议所有可能阻塞的 channel、锁、WaitGroup 操作都画一个生命周期图,明确每一步谁会唤醒、谁会超时。

协程泄漏没有银弹,但遵守三条原则能避开大部分坑:一是 channel 操作必须明确发送方和接收方的退出条件;二是 mutex 和 WaitGroup 尽量用 defer 保证释放;三是凡是涉及定时器、外部资源,都要考虑关闭或取消。做到这几点,你的 Go 服务至少能在高并发下稳定地呼吸。

本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-166.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 0

还没有回复,来抢沙发~