4.3. 内存泄漏排查

4.3.1. Go 中的内存泄漏

Go 有垃圾回收,但仍可能发生”逻辑泄漏”——对象仍被引用但不再使用。

4.3.2. 常见泄漏场景

4.3.2.1. 场景 1:Goroutine 泄漏

// ❌ Goroutine 永远阻塞
func leak() {
    ch := make(chan int)
    go func() {
        val := <-ch  // 永远阻塞
        fmt.Println(val)
    }()
    // ch 永远没有发送者
}

// ✅ 使用 context 控制
func noLeak(ctx context.Context) {
    ch := make(chan int)
    go func() {
        select {
        case val := <-ch:
            fmt.Println(val)
        case <-ctx.Done():
            return
        }
    }()
}

4.3.2.2. 场景 2:切片引用

// ❌ 小切片引用大数组
var global []byte

func leak() {
    big := make([]byte, 1<<20)  // 1MB
    // ... 填充数据
    global = big[:10]  // 只需要 10 字节,但 1MB 无法释放
}

// ✅ 复制需要的数据
func noLeak() {
    big := make([]byte, 1<<20)
    // ... 填充数据
    global = make([]byte, 10)
    copy(global, big[:10])
}

4.3.2.3. 场景 3:time.Ticker 未停止

// ❌ Ticker 泄漏
func leak() {
    ticker := time.NewTicker(time.Second)
    // 忘记调用 ticker.Stop()
}

// ✅ 确保停止
func noLeak() {
    ticker := time.NewTicker(time.Second)
    defer ticker.Stop()
    
    // 使用 ticker
}

4.3.2.4. 场景 4:资源未关闭

// ❌ 文件未关闭
func leak() {
    f, _ := os.Open("file.txt")
    // 忘记 f.Close()
}

// ✅ 使用 defer 关闭
func noLeak() {
    f, err := os.Open("file.txt")
    if err != nil {
        return
    }
    defer f.Close()
    
    // 使用文件
}

4.3.2.5. 场景 5:全局 Map 无限增长

// ❌ Map 无限增长
var cache = make(map[string]Data)

func process(key string, data Data) {
    cache[key] = data  // 只增不减
}

// ✅ 使用 LRU 或定期清理
import "github.com/hashicorp/golang-lru"

var cache, _ = lru.New(1000)  // 最多 1000 项

func process(key string, data Data) {
    cache.Add(key, data)  // 自动淘汰旧数据
}

4.3.2.6. 场景 6:闭包捕获

// ❌ 闭包捕获大对象
func leak() []func() {
    var funcs []func()
    for i := 0; i < 100; i++ {
        bigData := make([]byte, 1<<20)  // 1MB
        funcs = append(funcs, func() {
            _ = bigData  // 捕获 bigData
        })
    }
    return funcs  // 100 个 1MB 对象
}

// ✅ 只捕获需要的数据
func noLeak() []func() {
    var funcs []func()
    for i := 0; i < 100; i++ {
        bigData := make([]byte, 1<<20)
        summary := computeSummary(bigData)
        funcs = append(funcs, func() {
            _ = summary  // 只捕获摘要
        })
    }
    return funcs
}

4.3.3. 检测内存泄漏

4.3.3.1. 使用 pprof

# 获取堆分析
go tool pprof http://localhost:6060/debug/pprof/heap

# 比较两个时间点的堆
go tool pprof -base heap1.prof heap2.prof

4.3.3.2. 使用 runtime.MemStats

func monitorMemory() {
    var m runtime.MemStats
    for {
        runtime.ReadMemStats(&m)
        fmt.Printf("HeapAlloc = %v MB\n", m.HeapAlloc/1024/1024)
        fmt.Printf("NumGoroutine = %v\n", runtime.NumGoroutine())
        time.Sleep(10 * time.Second)
    }
}

4.3.3.3. 使用 goleak(测试中)

import "go.uber.org/goleak"

func TestMain(m *testing.M) {
    goleak.VerifyTestMain(m)
}

func TestNoLeak(t *testing.T) {
    defer goleak.VerifyNone(t)
    // 测试代码
}

4.3.4. 内存泄漏排查流程

        flowchart TD
    A[发现内存增长] --> B{确定泄漏类型}
    B -->|Goroutine 数量增长| C[pprof goroutine]
    B -->|堆内存增长| D[pprof heap]
    
    C --> E[找出阻塞的 goroutine]
    D --> F[找出分配来源]
    
    E --> G[检查 channel/锁/sleep]
    F --> H[检查是否有引用未释放]
    
    G --> I[修复代码]
    H --> I
    
    I --> J[验证修复]
    

4.3.5. 最佳实践

  1. 使用 context 控制 goroutine 生命周期

  2. 使用 defer 释放资源

  3. 避免全局容器无限增长

  4. 定期监控 goroutine 数量和内存

  5. 在测试中使用 goleak

  6. 使用有界的缓存(LRU)

4.3.6. 参考资源