前言

这是Go十大常见错误系列的第6篇:slice初始化常犯的错误。素材来源于Go布道者,现Docker公司资深工程师Teiva Harsanyi[1]

场景

假设我们知道要创建的slice的长度,你会怎么创建和初始化这个slice?
比如我们定义了一个结构体叫Bar,现在要创建一个slice,里面的元素就是Bar类型,而且该slice的长度是已知的。

方法1

有的人可能这么来做,先定义slice

  1. var bars []Bar
  2. bars := make([]Bar, 0)

每次要往bars这个slice插入元素的时候,通过append来操作

  1. bars = append(bars, barElement)

slice实际上是一个结构体类型,包含3个字段,分别是

  • array: 是指针,指向一个数组,切片的数据实际都存储在这个数组里。
  • len: 切片的长度。
  • cap: 切片的容量,表示切片当前最多可以存储多少个元素,如果超过了现有容量会自动扩容。

slice底层的数据结构定义如下:

  1. type slice struct {
  2. array unsafe.Pointer
  3. len int
  4. cap int
  5. }
  6. type Pointer *ArbitraryType

如果按照上面的示例先创建一个长度为0的slice,那在append插入元素的过程中,bars这个slice会做自动扩容。
如果bars的长度比较大,可能会发生多次扩容。每次扩容都要创建一个新的内存空间,然后把旧内存空间的数据拷贝过来,效率比较低。

方法2

在定义slice的时候指定长度,代码示例如下:

  1. func convert(foos []Foo) []Bar {
  2. bars := make([]Bar, len(foos))
  3. for i, foo := range foos {
  4. bars[i] = fooToBar(foo)
  5. }
  6. return bars
  7. }

这行代码 bars := make([]Bar, len(foos))直接指定了slice的长度,无需扩容。

方法3

在定义slice的时候提前指定容量,长度设置为0,代码示例如下:

  1. func convert(foos []Foo) []Bar {
  2. bars := make([]Bar, 0, len(foos))
  3. for _, foo := range foos {
  4. bars = append(bars, fooToBar(foo))
  5. }
  6. return bars
  7. }

这种方法也可以,也无需扩容。
那方法2和方法3哪种好一点呢?其实各有优缺点: