概念
切片是为了动态使用数组而创建的结构。我们知道数组创建之后,大小不可变。而在开发过程中,有时候会根据代码的实际运行结果,动态操作数组,改变数组的大小。直接创建一个足够大的数组会非常浪费内存,而创建内存大小恰好的数组是不现实的。所以引入了切片,切片在动态操作的过程中,如果容量不够会触发扩容。
基本结构
切片的结构如下:
编译期间的Slice
type Slice struct {
Elem *Type // element type
}
运行时的Slice
type Slice struct {
Data uintptr
Len int
Cap int
}
我们将编译期间的Slice看成Slice1,运行时的Slice看成Slice2。Slice1的源码位于src\cmd\compile\internal\types,Slice2的源码位于\src\internal\unsafeheader。为什么要把Slice的结构分成编译期间和运行时呢。个人理解(我们知道切片是为了动态的操作数组而引入的,在运行时,必然会改变、创建Slice的内部状态。并且切片的容量大多时候运行时才能确定,所以将切片的完整结构Slice2放在运行时创建。但是在编译期间,对切片的类型进行检查是必须的。所以就有了Slice1 和Slice1。Slice1用来存储元素类型,在编译期间进行检查,而Slice2存储切片的所有内部实现)。
初始化
不同于数组的两种初始化方式:arr := [2]int{1, 2}、arr := [...]int{1, 2}。切片有三种初始化方法:
arr [begin:end]或者slice[begin:end]. 在左闭右开区间[begin,end)截取数组或者切片获得新切片slice := []int{1,2,3}通过字面量初始化切片slice := make([]int, 5, 10)通过make关键字创建切片,5表示当前切片的长度,10表示切片的当前容量
对于三种初始化方法,第一种是最底层的方式。 注意:
- 注意1:使用
方法1初始化切片,新的切片仍然是使用原来的数组,在没有发生扩容之前,改变任意一个都会互相影响。 - 注意2:使用
方法2初始化切片,会被解析成两部分:创建数组、使用方式1操作数组初始化切片 - 注意3:使用
方法3初始化切片,会进行两个判断:切片是否足够小、是否发生逃逸。如果切片很小不发生逃逸,则和注意2类似只是少了字面量的赋值;反之,在堆上初始化切片,调用\go\src\runtime\slice.go:runtime.makeslice代码如下:
func makeslice(et *_type, len, cap int) unsafe.Pointer {
mem, overflow := math.MulUintptr(et.Size_, uintptr(cap))
// 运行时检测 1 内存是否溢出、2 分配内存是否足够、3 长度是否大于0小于容量
if overflow || mem > maxAlloc || len < 0 || len > cap {
mem, overflow := math.MulUintptr(et.Size_, uintptr(len))
if overflow || mem > maxAlloc || len < 0 {
panicmakeslicelen()
}
panicmakeslicecap()
}
// 分配内存
return mallocgc(mem, et, true)
}
追加和扩容
向切片中追加元素,如果长度超过容量则会触发扩容,扩容的代码位于\go\src\runtime\slice.go:growslice,如下:
func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice {
...
newcap := oldCap
doublecap := newcap + newcap
// 如果新长度大于两倍容量,则最终扩容容量为新长度
if newLen > doublecap {
newcap = newLen
} else {
// 如果老容量低于256,则双倍扩容
const threshold = 256
if oldCap < threshold {
newcap = doublecap
} else {
// 老容量高于256,则按照如下公式扩容
for 0 < newcap && newcap < newLen {
newcap += (newcap + 3*threshold) / 4
}
if newcap <= 0 {
newcap = newLen
}
}
}
...
}
需要注意的是,触发扩容之后,具体的内存计算还要考虑内存对齐,内存大小向上对齐之后,容量可能会发生细微的增长。
复制切片
切片的复制,不是依次复制,而是调用runtime.memmove直接进行整块复制。
参考文献:《Go语言设计与实现》 @Draven