intro
切片类型的声明方式与数组有一些相似,不过切片的长度是动态的,所以声明时只需要指定切片中的元素类型
编译器在编译期间为了简化对数组的操作,大多数操作都会直接读写内存的特定位置。
切片是运行时才会确定内容的结构。
编译期:切片生成的类型只会包含切片中的元素类型
运行时:切片可以由如下的 reflect.SliceHeader
结构体表示
type SliceHeader struct {
Data uintptr //Data 是指向数组的指针;
Len int //Len 是当前切片的长度;
Cap int // Cap 是当前切片的容量,即 Data 数组的大小:
}
Data
是一片连续的内存空间,这片内存空间用来存储切片中的全部元素。
容量与长度
切片是对数组中部分连续片段的引用,而作为数组的引用,我们可以在运行区间可以修改它的长度和范围。
当切片底层的数组长度不足时就会触发扩容,切片指向的数组可能会发生变化。
当修改底层数组时,原指向该数组的切片内容会发生变化
切片初始化方式
- 通过下标的方式获得数组或者切片的一部分(像python那样截取)
- 使用字面量初始化新的切片 编译期
- 使用关键字
make
创建切片 运行时
如果使用字面量的方式创建切片,大部分的工作都会在编译期间完成。但是使用 make
关键字创建切片时,很多工作都需要运行时的参与;调用方必须向 make
函数传入切片的大小以及可选的容量,类型检查期间会校验入参的正确性,检查过程不仅会检查 len
是否传入,还会保证传入的容量 cap
一定大于或者等于 len
当切片发生逃逸或者非常大时,运行时需要在堆上初始化切片,如果当前的切片不会发生逃逸并且切片非常小的时候,
make([]int, 3, 4)
会被直接转换成如下所示的代码:
var arr [4]int //先定义 后赋值
n := arr[:3] //这两部分操作都会在编译阶段完成
可以直接触发运行时错误的情况
- 内存空间的大小发生了溢出
- 申请的内存大于最大可分配的内存
- 传入的长度小于 0 或者长度大于容量
makeslice
在最后调用的 mallocgc
是用于申请内存的函数,如果遇到了比较小的对象会直接初始化在 Go 语言调度器里面的 P 结构中,而大于 32KB 的对象会在堆上初始化.
扩容策略
在分配内存空间之前需要先确定新的切片容量,运行时根据切片的当前容量选择不同的策略进行扩容:
- 如果期望容量大于当前容量的两倍就会使用期望容量;
- 如果当前切片的长度小于 1024 就会将容量翻倍;
- 如果当前切片的长度大于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量;
func growslice(et *_type, old slice, cap int) slice {
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
if newcap <= 0 {
newcap = cap
}
}
}
扩容操作并不是直接向系统申请内存,而是语言本身实现的内存分配模块会预先申请很多酷爱大小不一的内存。扩容操作会拿到一块能够满足于自身的内存空间,然后在这块内存空间上进行切片的扩容操作,这样做的好处是可以减少内存的碎片化,提高内存的利用率。
上述过程仅会确定切片的大致容量,还需要进一步根据切片中的元素大小对齐内存,内存函数会将待申请的内存向上取整,取整时会使用 runtime.class_to_size
数组,使用该数组中的整数可以提高内存的分配效率并减少碎片。
var class_to_size = [_NumSizeClasses]uint16{
0,
8,
16,
32,
48,
64,
80,
...,
}
summ
切片是一种功能强大的数据结构,在使用时需要注意大切片扩容或者复制时可能会发生大规模的内存拷贝,一定要减少类似操作避免影响程序的性能。