ARTICLE / Go · 内存泄漏 · 垃圾回收 · 性能优化
An English translation is not available yet. The Chinese original is shown below.
Go Slice 导致的内存泄漏:小切片如何拖垮大内存?
为什么一个只剩几个元素的 slice 能让几百 MB 内存迟迟不降?从 GC 的可达性原理出发,拆解「大底层数组被小切片绑架」与「残留指针未清零」两类内存泄漏,并给出 slices.Clone 深拷贝、clear 置零与 slices.Delete 的解法。
- Published
- Reading time
- 2 min read
- Author
- Nicolas Leigh
Go 有垃圾回收(GC),但很多开发者常遇到一个困惑:明明一个 slice 最终只留了几个元素,为什么几百 MB 内存死活不降?
这通常不是 runtime 的 bug,而是 GC 严格按照可达性(Reachability)工作的必然结果。最典型的坑有两个:大底层数组被小切片绑架,以及残留指针未清零。
1. 巨型数组被“偷窥孔”锁死
从大内存切一小段,看起来只有十几字节,但只要这个小切片还在用,整个大数组就无法释放:
func getHeader(data []byte) []byte {
return data[:10] // 虽然 len=10,但它依然挂靠在原底层数组上
}
data := make([]byte, 100*1024*1024) // 100 MB
header := getHeader(data)
data = nil // 没用,GC 依然无法回收那 100 MB,因为 header 还在引用它
注:用
data[:10:10]也没用。限制 cap 只能防止后续 append 踩踏原数组,并不能切断对大数组的引用。
解法:主动深拷贝,切断血缘关系
result := make([]byte, 10)
copy(result, data[:10]) // 或 append([]byte(nil), data[:10]...)
// 或者采用 Go 1.21 引入的 slices.Clone
result := slices.Clone(data[:10])
拿几十字节的额外分配代价,换来 100 MB 内存被 GC 及时回收,这笔账绝对划算。
2. 截断/删除切片时的“幽灵指针”
如果切片里存的是指针或带指针的结构体(如包含 []byte、map、string),缩短切片长度并不能释放被裁掉的对象:
type User struct {
Data []byte // 假设每个占 1 MB
}
users := make([]User, 1000)
// ... 填充数据 ...
// 业务上只要前两个:
users = users[:2]
此时 len(users) == 2,但后面 998 个元素依然安稳躺在底层数组里,它们的 Data 指针依然有效,近 1 GB 内存全被悬空锁死。
解法:显式置空或使用 clear
复用原数组截断:用 Go 1.21+ 引入的内置函数 clear,把不在视界内的槽位打回零值:
clear(users[2:]) // 将尾部元素置为零值,Data 字段因此变为 nil
users = users[:2]
单元素删除:经典元素位移后,末尾务必置空:
copy(s[i:], s[i+1:])
s[len(s)-1] = nil // 抹掉尾部残留指针,让 GC 回收
s = s[:len(s)-1]
// Go 1.21 引入了官方标准库 slices.Delete,更简单的方法如下
s = slices.Delete(s, i, i+1)
什么时候该当心?
不用草木皆兵——普通的 []int 或短生命周期的小切片,随函数退出自然会被扫掉。
真正需要警惕的是两个交集:
- 长生命周期(全局缓存、长期持有的 struct 字段、连接池);
- 大尺寸或含指针(网络解析大 Buffer、大文件切块、
[]*T)。
看到 s = s[:n] 时多问一句:这个底层数组到底有多大?它背后的指针,是不是该亲手切断了?