协程泄漏主因是高并发下裸启goroutine且未配context或错误兜底;须用带超时/取消的ctx、统一封装spawnTask、优先调Context版本API、多生产者channel关闭需配合WaitGroup。

协程泄漏常出现在高并发队列的启动路径里
直接用 go f() 启动任务协程,又没配 context.Context 或没做错误兜底,是泄漏最常见源头。尤其在 HTTP handler、消息消费循环、定时任务中——每次请求/每条消息都无节制启新协程,不出三天 runtime.NumGoroutine() 就会从几百飙到上万。
- 别写
go processMsg(msg)这种裸调用;必须传入带超时或取消信号的ctx - 所有异步任务入口,统一走封装函数,比如
spawnTask(ctx, fn),内部强制检查ctx.Err() != nil再执行 - 第三方库调用(如
db.Query、http.Do)优先选带Context版本(db.QueryContext、http.Do配ctx),否则阻塞时无法中断
channel 关闭不匹配导致接收协程永久卡住
用 for range ch 消费队列 channel 时,如果发送方忘记 close(ch),或者关闭时机不对(比如还没发完就关了),接收协程就会停在 range 头不动,状态永远是 chan receive。
- 只在明确“所有生产者都已完成”时才
close(ch);多生产者场景推荐用sync.WaitGroup+close组合 - 避免在
select里裸写,必须加default或超时分支,防止单次卡死拖垮整条流水线 - 若 channel 是 worker 间中转用,优先设缓冲(如
make(chan Task, 1024)),但缓冲大小要和实际吞吐匹配,别设成0或math.MaxInt
协程池没绑定生命周期,worker 卡死或漏释放
很多人以为引入 golang.org/x/sync/semaphore 或 gorush 就万事大吉,结果发现 sem.Acquire 成功后 panic 了,sem.Release 没执行,槽位永久丢失——池子越跑越小,新任务全在排队,最后堆积溢出。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
-
sem.Acquire必须配defer sem.Release(1),且defer要写在 goroutine 内部,不能靠外层闭包 - 别在每个 handler 里
new(semaphore.Weighted),它得是全局单例,和 server 生命周期一致 - worker 执行体必须
recover(),否则 panic 会导致该 worker 协程退出,池容量实质缩水 - 任务队列别用无缓冲 channel(
make(chan func())),那是伪队列,一塞满就阻塞提交方,容易引发级联超时
pprof 查泄漏时,重点盯这些阻塞态
当 runtime.NumGoroutine() 持续上涨,立刻抓 /debug/pprof/goroutine?debug=2。泄漏协程不会堆栈很深,而是反复出现同一行、状态固定不变:
-
chan receive:接收方在等没人发的 channel,或 channel 没 close -
chan send:尤其是无缓冲 channel,发送方卡在没接收者那 -
IO wait:HTTP client 没设Timeout、WebSocketWriteMessage卡住、TCP 连接未断开 -
select:块里没监听ctx.Done(),或ctx根本没传进去
真正难排查的是那些“看起来有 context、也写了 select”,但 ctx.Done() case 被其他 case 抢先就绪而永远不触发的情况——监听必须放第一个 case,且不能依赖闭包变量传 ctx。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










