概述
Slice(切片)代表变长的序列,序列中每个元素都有相同的类型。其实 slice 也就相当于动态数组,长度并不固定,可以用append追加元素,并且slice会在容量不足时自动扩容。
在go语言中文文档中,对于slice有这样的描述:
- 切片:切片是数组的一个引用,因此切片是引用类型。但自身是结构体,值拷贝传递。
- 切片的长度可以改变,因此,切片是一个可变的数组。
- 切片遍历方式和数组一样,可以用len()求长度。表示可用元素数量,读写操作不能超过该限制。
- cap可以求出slice最大扩张容量,不能超出数组限制。0 <= len(slice) <= len(array),其中array是slice引用的数组。
- 切片的定义:var 变量名 []类型,比如
var str []string
var arr []int
。- 如果 slice == nil,那么 len、cap 结果都等于 0。
slice的数据结构
根据runtime包下的slice.go源码可以看到,slice的基本结构如下:
type slice struct {
array unsafe.Pointer
len int
cap int
}
array
是指向第一个slice元素对应的底层数组元素的地址的指针;len
是当前切片的长度;cap
是当前切片的容量,即array
数组的大小:
注意这里len范围内的元素是可以安全访问的,超出这个范围的元素访问会导致panic
一些特性
1、
多个slice之间可以共享底层的数据,并且引用的数组部分区间可能重叠,如下图所示:
如果对共用的底层数组切片进行append添加元素,那么就会开辟新数组,不在共用底层数组,原来的元素拷贝过去,并且在新数组上添加新元素。
2、
和数组不同的是,slice之间不能比较,因此我们不能使用==操作符来判断两个slice是否含有全部相等元素。不过标准库提供了高度优化的bytes.Equal
函数来判断两个字节型slice是否相等(限于**[]byte**),但是对于其他类型的slice,我们必须自己展开每个元素进行比较
func equal(x, y []string) bool {
if len(x) != len(y) {
return false
}
for i := range x {
if x[i] != y[i] {
return false
}
}
return true
}
- 如果==使用的是浅度相等,只要两个slice的指针,长度和容量三个字段相等,那么两个slice就相等。但这样和数组不同的相等测试方法,会让人困惑,如:
func main() {
// 两个数组相等
a1 := [3]int{1,2,3}
a2 := [3]int{1,2,3}
fmt.Println(a1 == a2) // true
// 如果slice使用的是浅相等
a1 := []int{1,2,3}
a2 := []int{1,2,3}
fmt.Println(a1 == a2) // false,和数组的行为不同,造成困惑
}
- 如果==使用的是深度相等,和数组的行为保持一致,那也会有下面的问题。正常情况下,将一个slice赋值给另一个slice时,我们只是复制slice的结构体,两个slice的指针都指向同一个底层数组。
func main() {
s1 := s0
s1[0] = 0 // 通过s1修改,会影响到s0
}
slice唯一合法的比较操作是和nil比较
扩容规则
在slice.go下,有一个扩容growslice函数,当切片的容量不足时,便会调用该函数进行切片扩容,关键源码如下所示:
func growslice(et *_type, old slice, cap int) slice {
......
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.cap < 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 就会将容量翻倍;
- 如果当前切片的长度大于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量;
上述过程仅是进行了新容量的预估,接下来还需要根据切片中的元素大小对齐内存。
比如新容量是3,
int
类型,则它需要申请24B
的内存,此时它会向语言自身的内存管理模块去申请内存而内存管理模块会提前向操作系统申请一批内存,分为常用的规格管理起来,我们申请内存时,它会帮我们匹配到足够大,且最接近规格的内存,可能这里内存管理模块分配给你了
32B
的内存,所以这个时候新容量变成4个了
//runtime下sizeclasses.go文件,基本的内存单元如下 var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536, 1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768}
参考
- 《Go语言设计与实现》
- 《Go语言圣经》
- 幼麟实验室的Golang合辑
- Go语言中文文档