切片(Slice)是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。,支持自动扩容。
切片是一个引用类型,它的内部结构包含地址长度容量。稍后将根据切片的结构来推导它的一些特性。

切片创建和初始化

go语言中有几种方法可以创建和初始化切片

  • 通过切片字面量

通过切片字面量创建和初始化切片的定义格式和一维数组的很像,只是不需要指定长度而已。不过切片有长度和容量属性,可以用内置的len()和cap()函数获取切片的长度和容量。

  1. //length和cap都为3,元素为:10, 20, 30
  2. slice1 := []int{10, 20, 30}
  3. //指定index为10的值为30,其余的元素为零值,length和cap都为11
  4. slice3 := []int{10:30}
  • make函数创建

通过make函数创建的格式为make([]T, size, cap),其中cap可以省略,默认是和size相等。

  1. //length和cap都为5
  2. slice3 := make([]int,5)
  3. //length为5,cap为10
  4. slice4 := make([]int,5,10)

当使用make 时,需要传入一个参数,指定切片的长度

  • 通过切片表达式创建
    可以通过切片表达式的从已有的数组或者切片构造子切片(基于已有的字符串也能使用切片表达式).切片表达式的格式为a[low : high : max],a指代的是已有的数组或者切片。low表示开始的索引值,hign表示结束的索引值,max是为了控制新切片的容量,max-low就是新切片的实际容量,至于为什么稍后会解释。
    这三个索引值是有大小关系的:0 <= low <= high <= max <= 源数组的长度或者源切片的容量。max可以省略不写,当max省略的时候,可理解成max取的是源数组的长度或者源切片的容量。看如下例子:
  1. var arra1 = [5]int{31,13,14,33,23}
  2. slice5 := arra1[1:3:5]
  3. slice6 := arra1[1:3]
  4. fmt.Printf("t:%v len(t):%v cap(t):%v\n", slice5, len(slice5), cap(slice5))
  5. fmt.Printf("t:%v len(t):%v cap(t):%v\n", slice6, len(slice6), cap(slice6))
  6. 输出结果:
  7. t:[13 14] len(t):2 cap(t):4
  8. t:[13 14] len(t):2 cap(t):4

切片的本质

切片(slice)是对数组一个连续片段的引用(该数组称之为相关数组,通常是匿名的)。通过查看go源码的runtime包下可以看到如下对slice的定义:

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

可以看到slice层是一个结构体,包含三个字段,其中array保存的就是相关数组的地址,在初始化一个切片的时候,实际是会先分配相关数组的内存,然后切片会保存相关数组的地址,看如下代码:

  1. slice1 := []int{10, 20, 30}
  2. slice2 := []int{10, 20, 30}
  3. fmt.Printf("%p\n", &slice1)
  4. fmt.Printf("%p\n", &slice2)
  5. fmt.Printf("%p\n", slice1)
  6. fmt.Printf("%p\n", &slice1[0])
  7. fmt.Printf("%p\n", &slice1[1])
  8. 输出结果:
  9. 0xc000004480 //十进制:17536
  10. 0xc0000044a0 //十进制:17568
  11. 0xc0000103a0 //十进制:66464
  12. 0xc0000103a0 //十进制:66464
  13. 0xc0000103a8 //十进制:66472

上面代码连续定义了两个切片,通过地址分析我们可以看到他们相差32字节,我很郁闷,按照silence的底层结构,array占8字节,len和cap也是分别占8个字节,总字节应该是24才合适(我是安装C的计算方法),这和我预期不符合,暂时也没有找到合理的解释,先todo吧

由于slice本生就是引用类型,他保存的值就是一个指针,所以直接打印其地址,我们发现slice1指向的地址和slice1[0]的地址是一样的。而&slice1[0]的地址就是相关数组在内存中的起始地址。

举个例子,现在有个数组a := [8]int{0, 1, 2, 3, 4, 5, 6, 7}
切片s1 := a[:5]的内存示意图:
Clipboard_2020-10-15-09-10-03.png

切片s2 := a[3:6]的内存示意图:
Clipboard_2020-10-15-09-12-25.png

由此可以看出:
s1和s2都是公用一个相关数组a,只是他们保持的数组的起始地址不一样,由于这个结果会导致s1和s2的操作是会互相影响的。并且由于没有通过max索引去限制切片的cap,导致s1和s2在size长度之外还有公用的部分,这样的话使用append()也会出问题,稍后会讲到。

切片添加元素

通过append()可以为切片动态添加元素。 可以一次添加一个元素,可以添加多个元素,也可以添加另一个切片中的元素(后面加…)

  1. slice1 := []int{10, 20, 30}
  2. slice2 := []int{10, 20, 30}
  3. slice1 = append(slice1,slice2...) //[10 20 30 10 20 30]
  4. slice1 = append(slice1,11) //[10 20 30 10 20 30 11]

在调用append函数的时候会返回一个切片,这是由于切片的容量不足以容纳添加的元素个数的时候,底层会按一定的策略自动扩容,所谓扩容,实际是就是更换该切片指向的底层数组,底层数组的长度会按照一定的规则进行扩大。所以我们通常都需要用原变量接收append函数的返回值。看如下例子:

  1. var numSlice []int
  2. for i := 0; i < 10; i++ {
  3. numSlice = append(numSlice, i)
  4. fmt.Printf("len:%d cap:%d ptr:%p\n", len(numSlice), cap(numSlice), numSlice)
  5. }
  6. 输出结果:
  7. len:1 cap:1 ptr:0xc00000a0b8
  8. len:2 cap:2 ptr:0xc00000a0e0
  9. len:3 cap:4 ptr:0xc000010400
  10. len:4 cap:4 ptr:0xc000010400
  11. len:5 cap:8 ptr:0xc00000e280
  12. len:6 cap:8 ptr:0xc00000e280
  13. len:7 cap:8 ptr:0xc00000e280
  14. len:8 cap:8 ptr:0xc00000e280
  15. len:9 cap:16 ptr:0xc000018100
  16. len:10 cap:16 ptr:0xc000018100

从打印结果可以看到 ,当容量不足的时候会触发扩容,每次扩容,容量会翻倍,切片指向的地址也会发生改变。对于容量增加的策略,查看$GOROOT/src/runtime/slice.go的源码可以知道,当旧切片容量小于1024的时候,新切片的容量是旧切片的两倍,反之,以旧切片容量的四分之一进行递加,直到能容纳新的元素。