Goroutine 泄漏:原因、检测与防范
Goroutine 泄漏:原因、检测与防范
防止 Goroutine 泄漏的核心是:每个 goroutine 都应有明确的退出路径,使用 context 控制生命周期,用 select 加超时保护 channel 操作,用 WaitGroup 等待完成,并持续监控数量变化。
一、什么是 Goroutine 泄漏?
Goroutine 泄漏是指 goroutine 因永久阻塞而无法正常退出,一直占用内存和资源,最终导致程序 OOM 或性能下降。
常见泄漏场景
// ❌ 场景1:从空 channel 接收
ch := make(chan int)
data := <-ch // 永远阻塞(没人发送)
// ❌ 场景2:向已满的 channel 发送
ch := make(chan int, 1)
ch <- 1
ch <- 2 // 永远阻塞(没消费者)
// ❌ 场景3:等待不会发生的关闭
for v := range ch { // 永远等不到 close
fmt.Println(v)
}
// ❌ 场景4:goroutine 死循环无退出
go func() {
for {
doWork() // 永远运行,无法停止
}
}()
二、核心解决方案
1. 使用 Context 控制生命周期(推荐)
func worker(ctx context.Context, ch <-chan int) {
for {
select {
case data, ok := <-ch:
if !ok {
return
}
process(data)
case <-ctx.Done():
fmt.Println("worker 退出:", ctx.Err())
return
}
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
ch := make(chan int)
go worker(ctx, ch)
ch <- 1
ch <- 2
// 5秒后自动退出
}
2. 使用 WaitGroup 确保完成
func main() {
var wg sync.WaitGroup
ch := make(chan int, 10)
// 生产者
wg.Add(1)
go func() {
defer wg.Done()
defer close(ch)
for i := 0; i < 5; i++ {
ch <- i
}
}()
// 消费者
wg.Add(1)
go func() {
defer wg.Done()
for data := range ch {
fmt.Println(data)
}
}()
wg.Wait() // 等待所有 goroutine 完成
fmt.Println("所有 goroutine 已退出")
}
3. 所有 Channel 操作用 Select 保护
// ✅ 安全发送
func safeSend(ch chan<- int, data int, ctx context.Context) error {
select {
case ch <- data:
return nil
case <-ctx.Done():
return ctx.Err()
}
}
// ✅ 安全接收
func safeReceive(ch <-chan int, ctx context.Context) (int, error) {
select {
case data, ok := <-ch:
if !ok {
return 0, fmt.Errorf("channel 已关闭")
}
return data, nil
case <-ctx.Done():
return 0, ctx.Err()
}
}
4. 监控 Goroutine 数量
func monitor() {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for range ticker.C {
count := runtime.NumGoroutine()
fmt.Printf("当前 goroutine 数量: %d\n", count)
if count > 100 {
log.Printf("警告: goroutine 数量过多 (%d)", count)
// 发送告警
}
}
}
三、完整示例:安全的 Worker Pool
package main
import (
"context"
"fmt"
"sync"
"time"
)
type Job struct {
ID int
}
type Pool struct {
jobCh chan Job
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
closeOnce sync.Once
}
func NewPool(workers, queueSize int) *Pool {
ctx, cancel := context.WithCancel(context.Background())
p := &Pool{
jobCh: make(chan Job, queueSize),
ctx: ctx,
cancel: cancel,
}
for i := 0; i < workers; i++ {
p.wg.Add(1)
go p.worker(i)
}
return p
}
func (p *Pool) worker(id int) {
defer p.wg.Done()
for {
select {
case job, ok := <-p.jobCh:
if !ok {
return
}
fmt.Printf("Worker %d 处理 Job %d\n", id, job.ID)
time.Sleep(100 * time.Millisecond)
case <-p.ctx.Done():
fmt.Printf("Worker %d 退出\n", id)
return
}
}
}
func (p *Pool) Submit(job Job) bool {
select {
case p.jobCh <- job:
return true
case <-p.ctx.Done():
return false
default:
fmt.Println("队列满,丢弃 Job:", job.ID)
return false
}
}
func (p *Pool) Shutdown(timeout time.Duration) {
p.closeOnce.Do(func() {
close(p.jobCh)
done := make(chan struct{})
go func() {
p.wg.Wait()
close(done)
}()
select {
case <-done:
fmt.Println("正常关闭")
case <-time.After(timeout):
fmt.Println("超时,强制关闭")
p.cancel()
}
})
}
func main() {
pool := NewPool(3, 5)
// 提交任务
for i := 0; i < 20; i++ {
pool.Submit(Job{ID: i})
}
// 优雅关闭
pool.Shutdown(2 * time.Second)
fmt.Println("程序结束")
}
四、快速自查清单
□ 每个 goroutine 都有退出路径吗?
□ 使用了 context 控制生命周期吗?
□ channel 操作用 select 包裹了吗?
□ 有超时保护吗(time.After)?
□ 使用 WaitGroup 等待完成吗?
□ 监控 goroutine 数量变化吗?
□ 确保谁创建谁关闭 channel 吗?
□ 使用 sync.Once 防止重复关闭吗?
五、总结
| 方案 | 用途 | 关键代码 |
|---|---|---|
| Context | 生命周期控制 | ctx.Done() |
| Select | 非阻塞/超时操作 | select { case ... } |
| WaitGroup | 等待完成 | wg.Add/Done/Wait |
| 超时 | 防止永久阻塞 | time.After |
| 监控 | 提前发现问题 | runtime.NumGoroutine() |
核心原则
每个 goroutine 都必须知道"何时退出",而不是"是否会退出"。 使用 context、select、WaitGroup 组合,让退出路径清晰可控,并持续监控数量变化。

浙公网安备 33010602011771号