goroutine泄漏是指协程启动后因无退出路径而永久阻塞,导致runtime.numgoroutine()持续上涨、内存缓慢爬升,最终引发调度器过载或oom;典型成因包括未读尽channel、select漏写case、无缓冲channel单向操作等。

goroutine 泄露不是“协程没被回收”,而是启动了但永远无法退出的 goroutine 积压在运行时中。它不报错、不 panic,程序照常响应请求,但 runtime.NumGoroutine() 持续上涨,内存缓慢爬升,最终调度器吃紧、OOM 或服务僵死。
本质是生命周期失控:你起了一个 goroutine,却没给它明确的退出路径。
为什么 runtime.NumGoroutine() 看起来“涨得慢”却很危险
单个 goroutine 初始栈约 2KB,看似无害。但泄漏往往呈线性增长——每处理 1 个请求就多留 2–5 个卡住的 goroutine,1 小时后就是上千个,占用数 MB 栈内存 + 钉住的堆对象(比如未关闭的 http.Response.Body、未释放的 sql.Rows)。更隐蔽的是:这些 goroutine 多数处于 chan send、select、semacquire 状态,pprof/heap 不会体现它们,只有 /debug/pprof/goroutine?debug=2 能暴露。
select 里漏写 case 是最常见硬伤
很多人以为传了 context.Context 就安全了,其实关键在协程内部是否真监听并响应它。
- 错误写法:
go func() { time.Sleep(5 * time.Second); doWork() }()—— 完全无视ctx - 半吊子写法:
go func(ctx context.Context) { select { case —— 缺少 <code>case ,超时前 cancel 也无效 - 正确写法:
go func(ctx context.Context) { select { case
注意:如果 doWork() 本身也阻塞(如 http.Do),它还得自己支持 ctx;否则你只是把泄漏点往后推了一层。
向无缓冲 chan 发送数据却不确保有人接收
这是 Go Tour Same() 练习题的经典翻车现场:
-
w := make(chan int)创建无缓冲通道 -
go Walk(t, w)启动协程往w发送值 - 主逻辑提前
return,w再无人接收 -
Walk在递归中执行ch → 永久阻塞 → <code>goroutine卡死
修复不是“加缓冲”,而是引入协作式退出机制:用 quit chan struct{} 或 ctx.Done() 在发送前做 select 防御,让 Walk 能感知取消并主动 return。
真正难排查的不是“哪一行代码起的 goroutine”,而是谁该负责关掉它、何时关、关之前要不要清理资源。比如一个 http.Client 轮询任务,cancel 后不仅要退出循环,还得关闭已打开的连接、清空待发 channel、重置重试计数器——漏掉任何一环,都可能让 goroutine 表面退出,实则留下 dangling resource,为下次泄漏埋雷。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











