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 组合,让退出路径清晰可控,并持续监控数量变化。

posted @ 2026-07-12 16:56  若-飞  阅读(4)  评论(0)    收藏  举报