Slice是Go语言提供的一种常用的动态数据结构,相比数组,其长度并不固定,当我们向切片中添加元素时,如果容量不足就会自动扩容。
可以通过三种方式声明一个切片:
- 利用下标获取数组或切片的一部分
- 使用字面量初始化新的切片
- 使用关键字make创建切片
1 | //1 |
切片在编译期间生成的类型只会包含元素类型,如int
、interface{}
等。
数据结构
运行期间的Slice数据结构包含三个部分:
Data
:指向数组的指针Len
:当前切片的长度Cap
:当前切片的容量
从切片的结构中可以看出,Slice是对底层数据的一层抽象引用,上层只需要和切片交互而不关心底层数组的变化
扩容
我们可以通过append
向切片中添加元素,如果追加元素后切片大小超过容量,那么就会触发自动扩容。注意使用slice = append(slice,1,2,3)
这种追加方式使得扩容后的切片覆盖原切片。
当切片容量不足时,就会为切片分配新的内存空间并拷贝原切片中的元素,运行时根据切片的当前容量选择不同的扩容策略:
如果期望容量大于当前容量两倍,则使用期望容量;
如果当前切片长度小于1024,则将容量翻倍;
如果切片长度大于1024,则每次增加25%的容量,直到新容量大于期望容量。
此外,还需根据切片中元素大小进行内存对齐,将待申请的内存进行向上取整。
1 | var arr []int64 |
如上述Slice中期望容量为40字节,但因为切片中的元素大小等于 sys.PtrSize
,会向上取整申请内存大小为48字节,新切片容量为6.
拷贝
Go语言中的拷贝分为浅拷贝和深拷贝,对于像=
或[:]
下标方式对切片的浅拷贝操作,只需要拷贝三个字段;而通过Go语言内置的copy()
函数,则会进行深拷贝,涉及对底层数组的拷贝,理论上开销更大。
遍历
Go语言提供了range
用于for
循环遍历Slice,主要有两种方式:
1 | // 遍历下标和对应值 |
使用range
遍历切片时,变量v
是切片中数据的拷贝,因此修改拷贝数据不会影响原切片。
1 | type User struct { |
参数传递
Slice在作为函数参数传递时应特别注意,Go语言参数传递只有值传递,传递一个切片就会浅拷贝原切片,但因为指向的是同一个底层数组,函数内对切片元素的修改会影响到函数外的切片,如:
1 | func main() { |
但如果切片指向底层数组的指针被覆盖或修改(copy、append触发扩容),则此时函数内部和外部的Slice不再指向同一个数组,对函数内Slice的修改不会影响外部的Slice。