切片(Slice)是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。,支持自动扩容。
切片是一个引用类型,它的内部结构包含地址
、长度
和容量
。稍后将根据切片的结构来推导它的一些特性。
切片创建和初始化
go语言中有几种方法可以创建和初始化切片
- 通过切片字面量
通过切片字面量创建和初始化切片的定义格式和一维数组的很像,只是不需要指定长度而已。不过切片有长度和容量属性,可以用内置的len()和cap()函数获取切片的长度和容量。
//length和cap都为3,元素为:10, 20, 30
slice1 := []int{10, 20, 30}
//指定index为10的值为30,其余的元素为零值,length和cap都为11
slice3 := []int{10:30}
- make函数创建
通过make函数创建的格式为make([]T, size, cap)
,其中cap可以省略,默认是和size相等。
//length和cap都为5
slice3 := make([]int,5)
//length为5,cap为10
slice4 := make([]int,5,10)
当使用make 时,需要传入一个参数,指定切片的长度
- 通过切片表达式创建
可以通过切片表达式的从已有的数组或者切片构造子切片(基于已有的字符串也能使用切片表达式).切片表达式的格式为a[low : high : max]
,a指代的是已有的数组或者切片。low表示开始的索引值,hign表示结束的索引值,max是为了控制新切片的容量,max-low就是新切片的实际容量,至于为什么稍后会解释。
这三个索引值是有大小关系的:0 <= low <= high <= max <= 源数组的长度或者源切片的容量。max可以省略不写,当max省略的时候,可理解成max取的是源数组的长度或者源切片的容量。看如下例子:
var arra1 = [5]int{31,13,14,33,23}
slice5 := arra1[1:3:5]
slice6 := arra1[1:3]
fmt.Printf("t:%v len(t):%v cap(t):%v\n", slice5, len(slice5), cap(slice5))
fmt.Printf("t:%v len(t):%v cap(t):%v\n", slice6, len(slice6), cap(slice6))
输出结果:
t:[13 14] len(t):2 cap(t):4
t:[13 14] len(t):2 cap(t):4
切片的本质
切片(slice)是对数组一个连续片段的引用(该数组称之为相关数组,通常是匿名的)。通过查看go源码的runtime包下可以看到如下对slice的定义:
type slice struct {
array unsafe.Pointer
len int
cap int
}
可以看到slice层是一个结构体,包含三个字段,其中array保存的就是相关数组的地址,在初始化一个切片的时候,实际是会先分配相关数组的内存,然后切片会保存相关数组的地址,看如下代码:
slice1 := []int{10, 20, 30}
slice2 := []int{10, 20, 30}
fmt.Printf("%p\n", &slice1)
fmt.Printf("%p\n", &slice2)
fmt.Printf("%p\n", slice1)
fmt.Printf("%p\n", &slice1[0])
fmt.Printf("%p\n", &slice1[1])
输出结果:
0xc000004480 //十进制:17536
0xc0000044a0 //十进制:17568
0xc0000103a0 //十进制:66464
0xc0000103a0 //十进制:66464
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]的内存示意图:
切片s2 := a[3:6]的内存示意图:
由此可以看出:
s1和s2都是公用一个相关数组a,只是他们保持的数组的起始地址不一样,由于这个结果会导致s1和s2的操作是会互相影响的。并且由于没有通过max索引去限制切片的cap,导致s1和s2在size长度之外还有公用的部分,这样的话使用append()也会出问题,稍后会讲到。
切片添加元素
通过append()可以为切片动态添加元素。 可以一次添加一个元素,可以添加多个元素,也可以添加另一个切片中的元素(后面加…)
slice1 := []int{10, 20, 30}
slice2 := []int{10, 20, 30}
slice1 = append(slice1,slice2...) //[10 20 30 10 20 30]
slice1 = append(slice1,11) //[10 20 30 10 20 30 11]
在调用append函数的时候会返回一个切片,这是由于切片的容量不足以容纳添加的元素个数的时候,底层会按一定的策略自动扩容,所谓扩容,实际是就是更换该切片指向的底层数组,底层数组的长度会按照一定的规则进行扩大。所以我们通常都需要用原变量接收append函数的返回值。看如下例子:
var numSlice []int
for i := 0; i < 10; i++ {
numSlice = append(numSlice, i)
fmt.Printf("len:%d cap:%d ptr:%p\n", len(numSlice), cap(numSlice), numSlice)
}
输出结果:
len:1 cap:1 ptr:0xc00000a0b8
len:2 cap:2 ptr:0xc00000a0e0
len:3 cap:4 ptr:0xc000010400
len:4 cap:4 ptr:0xc000010400
len:5 cap:8 ptr:0xc00000e280
len:6 cap:8 ptr:0xc00000e280
len:7 cap:8 ptr:0xc00000e280
len:8 cap:8 ptr:0xc00000e280
len:9 cap:16 ptr:0xc000018100
len:10 cap:16 ptr:0xc000018100
从打印结果可以看到 ,当容量不足的时候会触发扩容,每次扩容,容量会翻倍,切片指向的地址也会发生改变。对于容量增加的策略,查看$GOROOT/src/runtime/slice.go
的源码可以知道,当旧切片容量小于1024的时候,新切片的容量是旧切片的两倍,反之,以旧切片容量的四分之一进行递加,直到能容纳新的元素。