避免在 Go 中使用 append

append 是我们向切片添加元素时的首选函数,但这可能不是最好用法。原因如下:

首先,我们创建两个函数,功能是将字符 “x” 填充进一个字符串切片。

WithAppend 调用 append 将 “x” 添加到一个字符串切片中

1
2
3
4
5
6
7
8
func WithAppend() []string {
var l []string
for i := 0; i < 100; i++ {
l = append(l, "x")
}

return l
}

WithAssignAlloc 通过用 make 来创建一个指定大小的字符串切片,之后赋值 “x” 给指定索引位而不是使用 append

1
2
3
4
5
6
7
8
func WithAssignAlloc() []string {
l := make([]string, 100)
for i := 0; i < 100; i++ {
l[i] = "x"
}

return l
}

这两个函数返回相同的结果,但其实现方式完全不同。

现在,让我们对这些函数进行基准测试。

1
2
3
4
5
6
7
8
9
10
11
12
func BenchmarkWithAppend(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
WithAppend()
}
}
func BenchmarkWithAssignAlloc(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
WithAssignAlloc()
}
}

结果如下:

1
2
3
4
BenchmarkWithAppend
BenchmarkWithAppend-8 863949 1322 ns/op 4080 B/op 8 allocs/op
BenchmarkWithAssignAlloc
BenchmarkWithAssignAlloc-8 2343424 523 ns/op 1792 B/op 1 allocs/op

WithAppend 的性能最差,而 WithAssignAlloc 的性能最好,这个结论应该可以说服你应该避免 append 了吧?

但先别急着走。

我们再写一个使用 append 的函数,并通过指定大小和容量来创建一个字符串切片。

1
2
3
4
5
6
7
func WithAppendAlloc() []string {
l := make([]string, 0, 100)
for i := 0; i < 100; i++ {
l = append(l, "x")
}
return l
}

再次运行基准测试。

1
2
3
4
5
6
BenchmarkWithAppend
BenchmarkWithAppend-8 863949 1322 ns/op 4080 B/op 8 allocs/op
BenchmarkWithAppendAlloc
BenchmarkWithAppendAlloc-8 2543119 514 ns/op 1792 B/op 1 allocs/op
BenchmarkWithAssignAlloc
BenchmarkWithAssignAlloc-8 2343424 523 ns/op 1792 B/op 1 allocs/op

现在我们在 WithAppendAllocWithAssignAlloc 上得到了同样好的性能。

为什么 WithAppend 性能很差?在使用 WithAppend 往切片中添加元素时,当切片的容量不足时,需要创建一个新的更大的切片来对切片进行扩容,这导致多次分配。


在你优化代码之前,应该通过基准测试来找到代码中的瓶颈。上面的例子过于简化,你可能并不总是知道应该提前分配切片的大小。

另外,过早地进行性能调整可能会矫枉过正。