对 slice 的使用方法和底层机制进行记录,以下内容基于 go1.22.1 版本。
slice 是 go 语言中类似 list 的存在,正式名字叫切片。注意切片和数组不是一个东西!
a := make([]int, 10, 20)
,这会创建一个长度为10,容量为20的切片,切片里的每个值会被初始化为对应类型的默认值。如果一个切片变量只声明不 make:var a []int
,那么变量值 a 为 nil。a := b[start:end]
的,名叫简单表达式的方式得到一个新的切片,两个切片共享内存空间,对 a 的修改也会影响 b(包括没有切到的,但实际还在 b 容量内的部分也会被影响),反之亦然。注意 start 和 end 的取值范围是0到 b 的容量而不是长度。切取的内容是包括b[start]
,但不包括b[end]
的。另外一提,字符串也支持这样的切取,但是字符串不会共享内存空间。
a := b[start:end:max]
的名叫扩展表达式的方式得到一个新的切片,该新切片和通过a := b[start:end]
得到的切片最大的不同是,前者的 cap 被锁死为max - end
。如果到达容量上限后再 append,会重新分配内存,而不影响原切片的内容。切片的底层实现是这样的:
gotype slice struct {
array unsafe.Pointer
len int
cap int
}
可以明显看出,切片实质上就是对数组指针的封装,所以函数可以放心传切片而不是切片指针。
runtime.nextslicemap
函数为策略进行扩容(实际扩容函数为runtime.growslice
)(扩容的时候也会进行内存对齐)。内存对齐是为了 CPU 访问内存次数更少设置的。因为内存是按字长进行存储和读取的,所以既然每次读写内存都是固定长度的数据,假设字长为8位,一个相同的,长度为10位的变量被切到3个字长里面(1位,8位,1位)和被切到2个字长(8位,2位)里面,当然是后者更有优势。这就是内存对齐的工作。
从这个机制出发,结构体成员变量的排列也能够影响其实际占据内存的大小,当然影响其实微乎其微,这一点会在结构体底层/特性解析中会说。
runtime.nextslicemap
函数如下:
go// nextslicecap 根据需求的新的长度和 slice 旧的容量 计算出扩容的目标容量
// 这个需求的新的长度 即为当前切片的容量加上追加元素的数量
// nextslicecap computes the next appropriate slice length.
func nextslicecap(newLen, oldCap int) int {
newcap := oldCap
doublecap := newcap + newcap
if newLen > doublecap { // 如果需求的新的长度超过两倍的现有的容量 直接返回新的长度作为容量
return newLen
}
const threshold = 256
if oldCap < threshold {
return doublecap // 如果现有的容量小于256 那么直接返回两倍现有的容量作为新容量
}
for {
// Transition from growing 2x for small slices
// to growing 1.25x for large slices. This formula
// gives a smooth-ish transition between the two.
// 对于小切片 容量增长速度应当是两倍
// 对于大切片 容量增长速度应当是1.25倍
// 下面的公式 中和了上面两个原则 并且使容量增长曲线更平滑
// 下面的公式在代码下面会用 LaTex 写出
newcap += (newcap + 3*threshold) >> 2
// We need to check `newcap >= newLen` and whether `newcap` overflowed.
// newLen is guaranteed to be larger than zero, hence
// when newcap overflows then `uint(newcap) > uint(newLen)`.
// This allows to check for both with the same comparison.
// 检查 newcap 是否大于 newLen 的同时 检查 newcap 是否发生了溢出
// 因为 newcap 是大于0的 所以当 uint(newcap) > uint(newLen) 时 可以认为 newcap 发生了溢出
// 注意 一个 int 存储的数字如果到达上限 再往上加就会发生溢出 值变为负数
// 溢出是越加 读到的值的绝对值越来越小
// 所以溢出时 用 uint 读到的值会非常非常大 配合下面的检查 newcap 本身是否小于0 即可判断 newcap 是否溢出
if uint(newcap) >= uint(newLen) {
break
}
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
// 如果 newcap 发生了溢出 直接返回 newLen 作为容量
if newcap <= 0 {
return newLen
}
return newcap
}
本文作者:御坂19327号
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!