Go底层原理--切片
切片的组成
// SliceHeader is the runtime representation of a slice.
// It cannot be used safely or portably and its representation may
// change in a later release.
// Moreover, the Data field is not sufficient to guarantee the data
// it references will not be garbage collected, so programs must keep
// a separate, correctly typed pointer to the underlying data.
SliceHeader 是切片的运行时表示形式。
它不能安全或便携地使用,其表示形式可能在更高版本中的更改。
此外,“数据”字段不足以保证数据它的引用不会被垃圾回收,所以程序必须保持指向基础数据的单独、正确键入的指针。
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
- Data是指向切片元素对应的底层数组元素的地址的指针
- Len表示切片中元素的数目
- Cap表示切片的容量
切片的初始化
切片字面量的初始化,会以数组的形式存储与静态区中。在使用make函数初始化时,如果make函数初始化了一个大于64KB的切片,那么这个切片会逃逸到堆中,在运行时调用makeslice函数创建切片,小于64KB的切片直接在栈中初始化。
切片值复制和数据引用
数组的复制是值复制,对于数组副本的修改不会影响原来的数组,但是对于切片的副本的修改会影响原来的切片。这说明切片副本与原始切片共用一个内存。
但是在Go语言中,切片的复制也是值复制,但是不同于复制整个切片,切片的值复制指的是对于运行时SliceHeader结构的复制。如图7-3所示,底层指针仍然指向相同的底层数据的数组地址。
切片扩容原理
func growslice(et *_type, old slice, cap int) slice {
//新容量是旧切片的容量
newcap := old.cap
//双倍容量是两个新(旧)容量相加
doublecap := newcap + newcap
if cap > doublecap {
//如果新申请的容量(cap)大于2倍的old.cap,新容量(newcap)就是申请的容量
newcap = cap
} else {
//当新申请的容量(cap)不大于2倍的old.cap
//定义一个常量threshold(门槛)长度为256
const threshold = 256
//如果旧容量小于256
if old.cap < threshold {
//新容量就是2倍旧容量
newcap = doublecap
} else {
//检查新容量(newcap)是否大于0并且新容量(newcap)是否小于申请的容量(cap),以防溢出并防止无限循环。
for 0 < newcap && newcap < cap {
//从小切片增长 2 倍过渡到大切片增长1.25倍。此公式在两者之间提供平滑的过渡。
newcap += (newcap + 3*threshold) / 4
}
//在以下情况下将新上限设置为请求的上限
if newcap <= 0 {
newcap = cap
}
}
}
return slice{p, old.len, newcap}
}
growslice函数会根据切片的类型,分配不同大小的内存。为了内存对齐,申请的内存可能存在大于实际的类型大小*容量大小。
如果切片需要扩容,那么最终需要到堆区申请内存。需要注意的是,扩容后新的切片不一定拥有新的地址。因此在使用append函数时,通常会采用a=append(a,T)的形式。根据et.ptrdata判断是否切片类型为指针,指向不同的逻辑。
当切片类型不是指针时,分配内存后只需要将内存后面的值清空,memmove(p,old.array,lenmem)函数用于将旧切片的值赋值给新的切片。整个过程的抽象如下。
old = make([]int,3,3)
new = append(old,1) => new = malloc(newcap*sizeof(int))
new[1]=old[1]
new[2]=old[2]
new[3]=old[3]
当切片类型为指针时,涉及垃圾回收写屏障开启时,对旧切片中指针指向的对象进行标记。