导航菜单

Goroutine 泄露

🔴 困难

题目描述

以下代码存在什么问题?如何修复?

func fetchData(ch chan<- int) {
    // 模拟耗时操作
    time.Sleep(time.Second)
    ch <- 42
}

func process() {
    ch := make(chan int)
    go fetchData(ch)
    
    // 如果这里提前返回或出错,goroutine 会泄露
    if someCondition {
        return
    }
    
    result := <-ch
    fmt.Println(result)
}

提示

  • goroutine 泄露是指 goroutine 无法退出,一直占用资源
  • channel 阻塞是常见原因
  • 使用 context 可以控制 goroutine 生命周期

解法

参考答案 (3 个标签)
goroutine context channel

问题分析

原代码的问题:

  1. goroutine 永久阻塞:如果 process() 提前返回,ch 没有接收者,fetchData 中的 ch <- 42 会永久阻塞
  2. 资源泄露:goroutine 占用的内存和栈空间无法释放
  3. 难以发现:不会立即 panic,而是逐渐消耗内存

修复方案

func fetchData(ctx context.Context, ch chan<- int) {
    result := doWork() // 模拟耗时操作
    
    select {
    case <-ctx.Done():
        fmt.Println("goroutine cancelled")
        return
    case ch <- result:
        fmt.Println("result sent")
    }
}

func process() error {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel() // 确保取消
    
    ch := make(chan int)
    go fetchData(ctx, ch)
    
    if someCondition {
        return errors.New("early return")
    }
    
    result := <-ch
    fmt.Println(result)
    return nil
}

常见泄露场景

1. Channel 阻塞

// ❌ 泄露:无接收者
func leak() {
    ch := make(chan int)
    go func() {
        ch <- 1 // 永远阻塞
    }()
}

// ✅ 修复:使用 context
func noLeak(ctx context.Context) {
    ch := make(chan int)
    go func() {
        select {
        case ch <- 1:
        case <-ctx.Done():
            return
        }
    }()
}

2. 无限循环

// ❌ 泄露:无限循环
func leak() {
    go func() {
        for {
            time.Sleep(time.Second)
            // 没有退出条件
        }
    }()
}

// ✅ 修复:添加退出条件
func noLeak(ctx context.Context) {
    go func() {
        ticker := time.NewTicker(time.Second)
        defer ticker.Stop()
        
        for {
            select {
            case <-ticker.C:
                // 处理任务
            case <-ctx.Done():
                return
            }
        }
    }()
}

3. Select 无 Default

// ❌ 泄露:所有 case 都阻塞
func leak() {
    ch1 := make(chan int)
    ch2 := make(chan int)
    
    go func() {
        select {
        case <-ch1: // 无发送者
        case <-ch2: // 无发送者
        }
        // 永久阻塞
    }()
}

// ✅ 修复:添加 default 或 context
func noLeak(ctx context.Context) {
    ch1 := make(chan int)
    ch2 := make(chan int)
    
    go func() {
        select {
        case <-ch1:
        case <-ch2:
        case <-ctx.Done():
            return
        }
    }()
}

检测 Goroutine 泄露

// 方法 1:运行时统计
func checkGoroutineCount() {
    fmt.Println(runtime.NumGoroutine())
}

// 方法 2:HTTP pprof
import _ "net/http/pprof"

func main() {
    go func() {
        log.Println(http.ListenAndServe("localhost:6060", nil))
    }()
    
    // 访问 http://localhost:6060/debug/pprof/goroutine?debug=2
}

// 方法 3:测试前后对比
func TestNoLeak(t *testing.T) {
    before := runtime.NumGoroutine()
    
    // 执行测试代码
    process()
    
    after := runtime.NumGoroutine()
    if after != before {
        t.Errorf("goroutine leak detected: %d -> %d", before, after)
    }
}

搜索