[Golang 修仙之路] Go基础:如何排查内存泄漏

21 阅读1分钟

内存泄漏的常见场景

  1. goroutine 泄漏:goroutine 创建后阻塞,不会退出。(解法:context超时控制)
  2. channel 中有数据堆积(无人消费)
  3. slice 长时间持有底层数组的引用。

goroutine 泄漏

// leak.go
package main

import (
	"fmt"
	"runtime"
	"time"
)

// 模拟每个请求都启动一个 goroutine 去等待结果,但结果永远不会发回来 -> 泄漏
func handleRequest(id int, respCh <-chan string) {
	go func() {
		// 这个 goroutine 阻塞在从 respCh 接收上,如果没有发送,永远不会退出
		// 常见场景:等待外部回调/超时处理忘了实现
		_ = <-respCh
		// if it ever receives, it would continue and exit
	}()
}

func main() {
	// 注意:respCh 是 unbuffered 且没有发送者
	respCh := make(chan string)
	ticker := time.NewTicker(500 * time.Millisecond)
	defer ticker.Stop()

	for i := 0; i < 1000; i++ {
		handleRequest(i, respCh)
		// 每 100 次打印一次 goroutine 数量
		if i%100 == 0 {
			fmt.Printf("after spawning %d goroutines, runtime.NumGoroutine() = %d\n",
				i, runtime.NumGoroutine())
		}
	}
	// 继续观察一段时间,NumGoroutine 会高于预期且不会下降
	for j := 0; j < 10; j++ {
		<-ticker.C
		fmt.Printf("tick %d: NumGoroutine = %d\n", j, runtime.NumGoroutine())
	}
	fmt.Println("done")
}

排查内存泄漏的思路

  1. 现象:CI阶段uber/go-leak发现了内存泄漏(比较协程池数量),或者内存随着时间升高。
  2. 开启 GODEBUG=gctrace=1,观察 GC 日志:如果 GC 频繁运行但内存占用依旧增长 → 高概率是内存泄漏。
  3. 通过pprof检查是对象没回收 还是 goroutine 没关闭。
  4. 通过pprof查看内存分配情况:pprof 中 top 和 火焰图 都有。
  5. 定位代码,解决问题。

排查内存泄漏的工具

go tool pprof http://localhost:6060/debug/pprof/heap
curl http://localhost:6060/debug/pprof/goroutine?debug=2 | less