一、定义
slice元素存储在连续的内存中,实质是数组
1 | // Slice is the runtime representation of a slice. |
二、创建切片
2.1 声明一个slice
只分配切片的结构,如下图,还没分配底层数组(存储空间),因此data=nil,存储个数为0,容量为0。这时不能对其进行读写操作,否则会越界。
1 | var ints []int |
data是数组的起始地址
2.2 通过make创建slice
如果通过make
创建slice,不仅会分配这3个slice结构,还会为ints开辟一段容纳5个整形元素的内存,并初始化为整形的默认值0。
1 | var ints []int = make([]int, 2, 5) // 长度是2 容量是5 |
已经存储的元素可以安全读写的(ints[0]、ints[1]、ints[2]),但是超出范围的话就是越界访问,会发生panic。
1 | ints[0] = 1 //正常读写 |
3.3 通过new创建slice
1 | ps := new([]string) |
1、new一个slice变量,同样会分配 data、len、cap,但是不负责底层数组的分配,因此data=nil,len=0,cap=0,并且不能进行读写。
2、new的返回是slice结构的起始地址,因此ps是个地址
3、由于目前还没分配底层数组,因此可以由append
来分配底层数组。
3.4 二维数组初始化
1 | var ans [][]int |
1 | grid := [][]int{ |
1 | ans := make([][]int, n) // n行 |
三、切片与底层数组
1 | arr := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} //定义数组,数组长度确定了之后不能改变 |
1、可以将不同的slice关联到同一个数组,它们会共用底层数组。len是加入到slice的元素个,而cap会从data开始到底层数组结束的个数
2、slice访问和修改的都是底层数组的元素,要注意越界问题。
1 | arr := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} |
可以通过append增加一个元素,或者修改范围arr[1:6]
1 | arr := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} |
3、添加元素后,容量不够,需要重新开辟数组
1 | arr := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} |
arr[7:]的底层数组是 len是3,cap是3,再添加一个元素则需要扩容,原来的底层数组不能使用了,需要开辟新的数组:原来的元素拷贝过来,再添加新元素。
四、切片的扩容规则
golang1.18 之前:
1 | ints := []int{1, 2} |
4.1 预估容量
1 | if oldCap * 2 < cap { |
所以例子中 newCap = 5
4.2 newCap个元素需要多大内存
在编程语言中,申请分配内存并不是直接与操作系统交涉,而是和语言自身实现的内存管理模块有关。内存管理模块会提前向OS申请 一批内存,分成常用的规格管理起来(8、16、32、48、64、96、112字节…)。
当我们申请内存时,会匹配到足够大 且最接近的规格。
4.3 匹配到适合的内存规格
在64位操作系统下,一个字节占用8位,因此需要申请 5 * 8 =40
(int 是8,string是16字节) 字节的内存,来存放到扩容后的底层数组,而实际申请时会匹配到48字节,因此48字节的内存能装6个元素。
所以扩容后的容量时6。
golang 1.18以后扩容规则改变了
1 | ints := []int{1, 2} |
4.1 预估容量
1 | if oldCap * 2 < cap { |
所以例子中 newCap = 5
五、实际应用(语法糖)
1、二维数组排序
2、unpack slice,…运算符
1 | var c = [...]int{1, 2, 3, 4, 5} // 不确定长度 |
1 | func main() { |
slice是一个结构体,作为形式参数传递时将结构体内容复制,实质上是值传递。
3、slice引用作为参数传递,实参不变
1 | func main() { |
4、slice指针作为参数传递,可以同步到实参
因为实参和形参指向同一个内存地址0x14000194018,因此 *sli = append(*sli, 6)
改变了slice的地址,其值会改变
1 | func main() { |