1. 切片究竟是什么

Go 语言数组是一个固定长度的、容纳同构类型元素的连续序列。因此 Go 数组类型具有两个属性:元素类型和数组长度。但凡这两个属性相同的数组类型是等价的。比如下面变量 a、b、c 对应的数组类型是三个不同的数组类型:

  1. var a [8]int
  2. var b [8]byte
  3. var c [9]int

切片之于数组就像是文件描述符之于文件。在 Go 语言中,数组更多是“退居幕后”,承担的是底层存储空间的角色;而切片则走向“前台”,为底层的存储(数组)打开了一个访问的“窗口”。

image.png

下面是切片在 Go 运行时(runtime)层面的内部表示:

  1. //$GOROOT/src/runtime/slice.go
  2. type slice struct {
  3. array unsafe.Pointer
  4. len int
  5. cap int
  6. }

在运行时中,每个切片变量都是一个 runtime.slice 结构体的实例。我们用下面语句创建一个 slice 实例:

  1. s := make([]byte, 5)

image.png

我们还可以创建对已存在数组进行操作的切片,这种语法 u[low:max] 称为数组的切片化

  1. u := [10]byte{11, 12, 13, 14, 15, 16, 17, 18, 19, 20}
  2. s := u[3:7]

image.png

我们可以为一个已存在数组建立多个操作数组的切片。

image.png

切片可以提供比指针更为强大的功能,比如下标访问、边界溢出校验、动态扩容等。

2. 切片的高级特性:动态扩容

切片类型是“部分”满足零值可用理念的,即零值切片也可以通过 append 预定义函数进行元素赋值操作:

  1. var s []byte // s被赋予零值nil
  2. s = append(s, 1)

新数组建立后,append 会把旧数组中的数据 copy 到新数组中,之后新数组便成为了 slice 的底层数组,旧数组会被垃圾回收掉。

  1. // slice_append.go
  2. var s []int // s被赋予零值nil
  3. s = append(s, 11)
  4. fmt.Println(len(s), cap(s)) //1 1
  5. s = append(s, 12)
  6. fmt.Println(len(s), cap(s)) //2 2
  7. s = append(s, 13)
  8. fmt.Println(len(s), cap(s)) //3 4
  9. s = append(s, 14)
  10. fmt.Println(len(s), cap(s)) //4 4
  11. s = append(s, 15)
  12. fmt.Println(len(s), cap(s)) //5 8

append 操作有些时候会给 Gopher 带来一些困惑,比如通过语法 u[low:max] 形式进行数组切片化而创建的切片,一旦切片 cap 触碰到数组的上界,再对切片进行 append,切片就会和原数组的解除“绑定”:

  1. // slice_unbind_orig_array.go
  2. package main
  3. import "fmt"
  4. func main() {
  5. u := []int{11, 12, 13, 14, 15}
  6. fmt.Println("array:", u) // [11, 12, 13, 14, 15]
  7. s := u[1:3]
  8. fmt.Printf("slice(len=%d, cap=%d): %v\n", len(s), cap(s), s) // [12, 13]
  9. s = append(s, 24)
  10. fmt.Println("after append 24, array:", u)
  11. fmt.Printf("after append 24, slice(len=%d, cap=%d): %v\n", len(s), cap(s), s)
  12. s = append(s, 25)
  13. fmt.Println("after append 25, array:", u)
  14. fmt.Printf("after append 25, slice(len=%d, cap=%d): %v\n", len(s), cap(s), s)
  15. s = append(s, 26)
  16. fmt.Println("after append 26, array:", u)
  17. fmt.Printf("after append 26, slice(len=%d, cap=%d): %v\n", len(s), cap(s), s)
  18. s[0] = 22
  19. fmt.Println("after reassign 1st elem of slice, array:", u)
  20. fmt.Printf("after reassign 1st elem of slice, slice(len=%d, cap=%d): %v\n", len(s), cap(s), s)
  21. }

运行这段代码,我们得到如下结果:

  1. $go run slice_unbind_orig_array.go
  2. array: [11 12 13 14 15]
  3. slice(len=2, cap=4): [12 13]
  4. after append 24, array: [11 12 13 24 15]
  5. after append 24, slice(len=3, cap=4): [12 13 24]
  6. after append 25, array: [11 12 13 24 25]
  7. after append 25, slice(len=4, cap=4): [12 13 24 25]
  8. after append 26, array: [11 12 13 24 25]
  9. after append 26, slice(len=5, cap=8): [12 13 24 25 26]
  10. after reassign 1st elem of slice, array: [11 12 13 24 25]
  11. after reassign 1st elem of slice, slice(len=5, cap=8): [22 13 24 25 26]

3. 尽量使用 cap 参数创建 slice

如何减少或避免为过多内存分配和拷贝付出的代价呢?一种有效的方法就是根据 slice 的使用场景在为新创建的 slice 赋初值时使用 cap 参数。

  1. s := make([]T, 0, cap)

下面是一个使用 cap 和不使用 cap 参数的 slice 的基准测试:

  1. // slice_benchmark_test.go
  2. package main
  3. import "testing"
  4. const sliceSize = 10000
  5. func BenchmarkSliceInitWithoutCap(b *testing.B) {
  6. for n := 0; n < b.N; n++ {
  7. sl := make([]int, 0)
  8. for i := 0; i < sliceSize; i++ {
  9. sl = append(sl, i)
  10. }
  11. }
  12. }
  13. func BenchmarkSliceInitWithCap(b *testing.B) {
  14. for n := 0; n < b.N; n++ {
  15. sl := make([]int, 0, sliceSize)
  16. for i := 0; i < sliceSize; i++ {
  17. sl = append(sl, i)
  18. }
  19. }
  20. }

下面是基本测试运行的结果(go 1.12.7,macbook pro i5 8 核,16G 内存):

  1. $go test -benchmem -bench=. slice_benchmark_test.go
  2. goos: darwin
  3. goarch: amd64
  4. BenchmarkSliceInitWithoutCap-8 50000 36484 ns/op 386297 B/op 20 allocs/op
  5. BenchmarkSliceInitWithCap-8 200000 9250 ns/op 81920 B/op 1 allocs/op
  6. PASS
  7. ok command-line-arguments 4.163s