提到切片,我们需要对比数组进行理解:
数组类型的值的长度是固定的,切片类型的值是可变长度的;
数组可以被叫做切片的底层数组,切片可以看作是对数组的某个连续片段的引用;
切片类型是引用类型,数组属于值类型;
len表示长度,cap表示容量。数组的容量等于长度;切片的容量不一定等于长度,切片的容量代表了底层数组的长度。
本文主要围绕切片的容量进行展开。
1)使用make初始化切片
s1:=make([]int,5,8) 指明容量,长度为5,容量是8
s2:=make([]int,5) 不指明容量,那么和长度是一样的,都是5
注意:make是专门用来创建slice,map,channel的值的,返回的是被创建的值,并且可以立即使用;new是申请一小块内存并标记它是用来存放某个值的,返回的是指向这块内存的指针,而且这块内存不会被初始化。
2)直接赋值初始化
s1:= []int{1, 2, 3, 4, 5, 6, 7, 8}
长度和容量都是8
3)在某切片上施加切片
s1:= []int{1, 2, 3, 4, 5, 6, 7, 8}
s2:= s1[3:6]
切片s2的长度为3,容量是5,值为[4,5,6]
说明:切片s2中的索引从0到2,指向的是切片s1及底层数组索引从3到5的值。切片s2和s1的底层数组是一样的,不过切片s2的容量是该底层数组从索引3开始到数组结束的元素个数。
题外话:因为切片s2和s1指向了同样的底层数组,那么需要注意什么呢?
测试代码:
s1:= []int{1, 2, 3, 4, 5, 6, 7, 8}
s2:= s1[3:6]
fmt.Printf("s1: %v (len: %d, cap: %d)\n",s1, len(s1), cap(s1))
fmt.Printf("s2: %v (len: %d, cap: %d)\n",s2, len(s2), cap(s2))
s2[0]=100 //s2[0]等同于s1[3]
fmt.Printf("s1: %v (len: %d, cap: %d)\n",s1, len(s1), cap(s1))
fmt.Printf("s2: %v (len: %d, cap: %d)\n",s2, len(s2), cap(s2))
测试打印:
s1: [1 2 3 4 5 6 7 8] (len: 8, cap: 8)
s2: [4 5 6] (len: 3, cap: 5)
s1: [1 2 3 100 5 6 7 8] (len: 8, cap: 8)
s2: [100 5 6] (len: 3, cap: 5)
看到测试代码演示结果,需要注意的是多个切片指向了同一个底层数组,当操作其中一个切片的时候是否会影响到其他指向同一个底层数组的切片。
4)扩容
不会改变原来的切片,而是会生成一个容量更大的切片,然后将原有的元素和新元素一并拷贝到新切片中。一般情况下,可以简单地认为新切片的容量是原切片容量的2倍,当原切片的长度大于等于1024,会是1.25倍。如果我们一次追加的元素过多,以至于使新长度比原容量的2倍还要大,那么新容量就会以新长度为基准。
参考runtime包中slice.go文件里的growslice函数的实现:
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
测试代码(这里以切片长度=1024为例,会发现新容量=旧容量*1.25):
s2 := make([]int, 1024)
println(s2)
s2 = append(s2, make([]int, 200)...)
println(s2)
测试结果:
[1024/1024]0xc000055f70
[1224/1280]0xc000058000
参考资料:Go语言核心36讲