goroutine泄漏典型表现为内存持续上涨且runtime.NumGoroutine()只增不减;可通过pprof查看goroutine profile,若大量goroutine阻塞在select、chan receive或semacquire,则大概率因channel未关闭或未消费导致。

goroutine泄漏的典型表现和快速确认方法
程序内存持续上涨、runtime.NumGoroutine() 返回值只增不减,且没有明显业务增长时,基本可以判定存在 goroutine 泄漏。更直接的方式是用 pprof 查看 goroutine profile:
curl 'http://localhost:6060/debug/pprof/goroutine?debug=2'如果输出中大量 goroutine 停留在
select、chan receive 或 semacquire 状态,大概率是阻塞在 channel 操作或锁上。
channel 未关闭或未消费导致的泄漏
向一个无缓冲 channel 发送数据,但没有 goroutine 接收,发送方会永久阻塞;有缓冲 channel 若容量填满后继续发送,同样阻塞。这类 goroutine 无法被调度器回收。
- 避免无缓冲 channel 的单向使用:如只启动 sender 不启 receiver,或 receiver 提前退出而 sender 不知情
- 用
select+default防止无限等待:select {
case ch <- data:
default:
// 丢弃或重试,避免阻塞
} - 对必须保证送达的场景,用带超时的
select:select {
case ch <- data:
case <-time.After(5 * time.Second):
log.Println("send timeout")
} - receiver 退出前,确保 sender 能感知 —— 通常靠额外的
donechannel 或 context 取消
context 被忽略或未传递到所有子 goroutine
只要 goroutine 中涉及 I/O、channel 操作或循环等待,就必须接收 ctx 并监听 ctx.Done()。漏掉任意一层,就可能让 goroutine 在父任务结束之后继续运行。
- 启动 goroutine 时,显式传入
ctx,不要依赖闭包捕获外层变量(容易误用已取消的 context) - 所有阻塞操作都应支持 cancel:用
ctx.Done()替代空select{},用http.NewRequestWithContext(ctx, ...)替代裸http.NewRequest - 注意第三方库是否真正响应
ctx—— 比如某些数据库驱动的QueryContext是必须显式调用的,Query不会受 context 控制
timer 和 ticker 未停止引发的隐性泄漏
time.Ticker 和 time.Timer 启动后会持续持有 goroutine,即使所属逻辑已退出。它们不会因为变量超出作用域而自动 stop。
立即学习“go语言免费学习笔记(深入)”;
- 每次调用
time.NewTicker或time.NewTimer后,务必在对应逻辑结束时调用ticker.Stop()或timer.Stop() - 在
select中监听ctx.Done()时,要确保Stop()被执行 —— 注意 defer 不会在 panic 后执行,建议用if !ticker.Stop() { 清空可能 pending 的 tick - 避免在循环中反复创建 ticker:应复用并控制启停,而不是“每轮 new 一个”
最易被忽略的是组合场景:比如一个 goroutine 同时读 channel、等 timer、发 HTTP 请求,三者都得各自处理 cancel,缺一不可。少监听一个 ctx.Done(),就等于埋下一颗泄漏定时炸弹。











