Go 中由切片引起的内存泄露

与 C/C++ 不同,Go 有 GC,所以我们不需要手动处理内存的分配和释放。不过,我们仍然应该谨慎对待内存泄漏问题。

来看一个由 slice 引起的内存泄漏案例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main
import (
"fmt"
)
type Object struct {}
func main() {
var a []*Object
for i := 0; i < 8; i++ {
a = append(a, new(Object))
}
fmt.Println(cap(a), len(a)) // 输出: 8, 8
a = remove(a, 5)
fmt.Println(cap(a), len(a)) // 输出: 8, 7
}
func remove(s []*Object, i int) []*Object {
return append(s[:i], s[i+1:]...)
}

我们可以看到,即使有一个对象被删除,a 的容量仍然是8,这意味着remove 函数可能导致潜在的内存泄漏。

为什么会发生这种情况?

来看一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main
import (
"fmt"
)
func main() {
// a 和 b 代表同一个数组 [1,2] 的两个部分
a := []int{1,2}
b := a[0:1]
fmt.Println(a, b) // 输出: [1 2] [1]

// 底层数组的容量是2,b在 append 后的长度将是2,只需将b的范围增加到数组[0:1]。
// array[1]将被改为3,因为 a 和 b 是在同一个数组上,所以a[1]也是3。
b = append(b, 3)
fmt.Println(a, b) // 输出: [1 3] [1 3]

// 因为 b 的长度将比数组的容量大3,所以将创建一个新的数组
// 新数组的容量将是 2*cap(old) = 4
b = append(b, 4)
b[0] = 0
// 现在 a 和 b 在不同的数组上
fmt.Println(a, b) // 输出: [1 3] [0 3 4]
fmt.Println(cap(a), cap(b)) // 输出: 2 4
}

如何避免内存泄漏?

在这种情况下,有两种内存泄漏。

1. 底层数组

底层数组的容量只会增加,但不会减少,第一个例子已经证明了这一点。

如果我们认为容量太大,我们可以创建一个新的 slice,并将原 slice 中的所有元素复制到新 slice 中。这是一个复制操作(时间)和内存使用(空间)之间的权衡。

1
2
3
4
5
6
func remove(s []*Object, i int) []*Object {
s = append(s[:i], s[i+1:]...)
a := make([]*Object, len(s))
copy(a, s) // 时间换空间
return a
}

2. 指向数组元素的内存,其类型为指针

解决方法:将未使用的元素设置为nil,它将会被 GC 释放。

1
2
3
4
5
6
func remove(s []*Object, i int) []*Object {
old := s
s = append(s[:i], s[i+1:]...)
old[len(old)-1] = nil
return s
}