内存泄漏排查
问题
Go 有 GC 为什么还会内存泄漏?常见的泄漏场景有哪些?如何排查?
答案
Go 中的"内存泄漏"
Go 有 GC,不会出现 C/C++ 那样的忘记 free 的问题。但 Go 的内存泄漏通常是对象仍被引用但不再需要,GC 无法回收。
最常见的是 goroutine 泄漏——goroutine 无法退出,其引用的内存永远不会释放。
常见泄漏场景
1. Goroutine 泄漏——Channel 阻塞
// ❌ goroutine 永远阻塞在 ch 发送上
func leak() {
ch := make(chan int)
go func() {
val := doWork()
ch <- val // 如果没人接收,goroutine 永远阻塞
}()
// 如果函数提前返回(如超时),goroutine 泄漏
select {
case result := <-ch:
use(result)
case <-time.After(1 * time.Second):
return // ch 无人接收,goroutine 泄漏!
}
}
// ✅ 修复:用带缓冲的 Channel 或 Context
func noLeak(ctx context.Context) {
ch := make(chan int, 1) // 带缓冲,发送不阻塞
go func() {
val := doWork()
ch <- val // 即使没人接收也不会阻塞
}()
select {
case result := <-ch:
use(result)
case <-ctx.Done():
return // goroutine 发送后自动退出
}
}
2. Goroutine 泄漏——忘记关闭 Channel
// ❌ 消费者 range 永远等待
func leak() {
ch := make(chan int)
go producer(ch) // 如果 producer 忘了 close(ch)
for v := range ch { // 永远不会结束
process(v)
}
}
3. time.After 内存泄漏
// ❌ 每次循环都创建 Timer,旧的 Timer 直到超时才会被 GC
for {
select {
case data := <-ch:
process(data)
case <-time.After(5 * time.Second): // 每次创建新 Timer
return
}
}
// ✅ 修复:复用 Timer
timer := time.NewTimer(5 * time.Second)
defer timer.Stop()
for {
timer.Reset(5 * time.Second)
select {
case data := <-ch:
process(data)
case <-timer.C:
return
}
}
4. 全局 Map 不清理
// ❌ map 只增不减
var cache = make(map[string]*BigObject)
func handler(key string) {
cache[key] = loadObject(key) // 永远不删除旧条目
}
// ✅ 修复:使用 LRU 缓存或定期清理
5. Slice 底层数组未释放
// ❌ s 虽然只有 2 个元素,但底层数组仍持有 1000000 个元素的引用
func getLast2(data []int) []int {
return data[len(data)-2:] // 底层共享大数组
}
// ✅ 修复:复制到新的 slice
func getLast2(data []int) []int {
result := make([]int, 2)
copy(result, data[len(data)-2:])
return result
}
6. HTTP 响应 Body 未关闭
// ❌ Body 未关闭,连接不会被复用,TCP 连接泄漏
resp, err := http.Get(url)
if err != nil { return err }
// 忘了 resp.Body.Close()
// ✅ 修复
resp, err := http.Get(url)
if err != nil { return err }
defer resp.Body.Close()
io.Copy(io.Discard, resp.Body) // 读完 Body,连接才能复用
排查工具
1. pprof 查看堆内存
# 查看当前堆使用
go tool pprof http://localhost:6060/debug/pprof/heap
# 比较两个时间点的差异
go tool pprof -diff_base=base.prof current.prof
2. pprof 查看 goroutine 数量
# 查看 goroutine 数量和栈
curl http://localhost:6060/debug/pprof/goroutine?debug=1 | head -5
# 详细分析
go tool pprof http://localhost:6060/debug/pprof/goroutine
(pprof) top
3. runtime.NumGoroutine()
// 定时打印 goroutine 数量
go func() {
for {
fmt.Println("goroutines:", runtime.NumGoroutine())
time.Sleep(10 * time.Second)
}
}()
4. goleak(测试中检测 goroutine 泄漏)
import "go.uber.org/goleak"
func TestMain(m *testing.M) {
goleak.VerifyTestMain(m) // 测试完成后检查是否有泄漏的 goroutine
}
常见面试问题
Q1: goroutine 泄漏的常见原因?
答案:
- Channel 阻塞:发送/接收没有对应的接收/发送方
- 忘记关闭 Channel:消费者
range永远不结束 - 缺少退出机制:没有 Context 取消 或 done Channel
- 锁等待:永远获取不到锁(死锁的一种形式)
Q2: 如何预防内存泄漏?
答案:
- goroutine 必须有退出条件——使用 Context 或 done Channel
- Channel 必须有 close——生产者负责关闭
- defer 释放资源——resp.Body.Close()、file.Close()、rows.Close()
- 全局缓存加上限——LRU、TTL 淘汰
- CI 中运行
go test -race和 goleak