切片的数据结构
- 切片本身并不是动态数组或数组指针。它内部实现的数据结构通过指针引用底层数组,设定相关属性将数据读写操作限定在指定的区域内。
- 切片本身是一个只读对象,其工作机制类似于数组指针的一种封装。
- 切片(slice)是对数组一个连续片段的引用,所以切片是一个引用类型。需要注意的是,终止索引标识的项不包括在切片内。切片提供了一个与指向数组的动态窗口。
- 切片的长度可以在运行时修改,最小为 0 最大为相关数组的长度:切片是一个长度可变的数组。
Slice的数据结构定义如下
type slice struct{
array unsafe.Pointer
len int
cap int
}
切片的结构体由3部分构成,Pointer 是指向一个数组的指针,len 代表当前切片的长度,cap 是当前切片的容量。cap 总是大于等于 len 的。
切片的扩容机制
- 首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)
- 否则判断,如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap)
- 否则判断,如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的 1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap)
- 如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)
由上图可知,预估容量为5,预估容量只是预估元素的个数,这么多元素需要多大内存呢?
内存容量 = 预估容量 *元素类型大小
上图中预估容量为 5 元素类型大小为int类型,64位为8个字节,所以预估内存大小为 5*8 = 40
那么分配内存容量就是40吗,并不是,在很多编程语言中,内存的分配并不是直接与操作系统交涉,而是与语言内存的管理模块,他会提前向操作系统中申请一批内存,分成常用的规格管理起来,如图
它会选择只够大而且接近的内存,所以选择48,48/8 = 6,所以扩容的容量为 6
使用make创建切片
使用make来创建Slice时,可以同时指定长度和容量,创建时底层会分配一个数组,数组的长度即容量
例如:slice := make([]int,5,10)
该Slice长度为5,即可以使用下标slice[0] ~ slice[4]来操作里面的元素,capacity为10,表示后续向slice添加新的元素时可以不必重新分配内存,直接使用预留内存即可。
使用数组创建slice
使用数组来创建Slice时,Slice将与原数组共用一部分内存。
案例:
func main() {
var array [10]int
var slice = array[5:6]
fmt.Println("lenth of slice: ", len(slice))//1
fmt.Println("capacity of slice: ", cap(slice))//5
fmt.Println(&slice[0] == &array[5])//true
}
slice根据数组创建,与数组共用存储空间所以slice[0]和array[5]的地址相同
例如,语句slice := array[5:7]所创建的Slice,结构如下图所示:
Slice Copy
使用copy()内置函数拷贝两个切片时,会将源切片的数据逐个拷贝到目的切片指向的数组中,拷贝数量取两个切片长度的最小值。
例如长度为10的切片拷贝到长度为5的切片时,将会拷贝5个元素。copy时不会发生扩容
注意
- 创建切片时可根据实际需要预分配容量,尽量避免追加过程中扩容操作,有利于提升性能;
- 切片拷贝时需要判断实际拷贝的元素个数
- 谨慎使用多个切片操作同一个数组,以防读写冲突
- 使用append()向切片中追加元素时有可能发生扩容,扩容时将会生成新的切片。