切片,相信大家用了 Go 语言那么久这这种数据类型并不陌生,但是平日里聊到关于切片是如何扩容的,很多人可能会张口就来,切片扩容的时候,如果老切片的容量小于 1024 那么就再扩容 1倍,也就是新的切片容量是老切片容量的两倍,同理,如果老切片容量大于 1024,那么就扩容1.25 倍
一个人这么说,多个人这么说,你可能就信了😂😂,可是大家都这么认为,我们就应该盲从吗?还是要自己去确认真实的扩容逻辑和实现方式,那就开始吧😁
结论先行,切片对于扩容并不一定是 2 倍,1.25倍,这个要看实际情况
本文分别从如下几点来聊聊切片的扩容
- 扩容是针对切片的,数组无法扩容
- 切片扩容到底是扩容到原来的几倍?
- 我们一般使用切片的时候可以如何避免频繁的扩容?
扩容是针对切片的,数组无法扩容
首先需要明确,数组是不能扩容的,数组定义的时候就已经是定长的了,无法扩容
切片是可以扩容的,我们可以通过 append 追加的方式来向已有的切片尾部进行追加,若原有切片已满,那么就会发生扩容
另外,我们知道数组是一段连续的内存地址,同一种数据类型的数据集合,例如这样
func main() {
log.SetFlags(log.Lshortfile)
var demoArray = [5]int{1, 2, 3, 4, 5}
log.Print("unsafe.sizeof(int) == ",unsafe.Sizeof(demoArray[0]))
for i, _ := range demoArray {
log.Printf("&demoAraay[%d] == %p", i, &demoArray[i])
}
}
图片
可以看到在这个案例的环境中,一个 int 类型的变量占用 8 个字节,自然对于 demoArray 数组中,地址是连续的,每一个元素占用的空间也是我们所期望的
那么切片的数据地址也是连续的吗??
如果有人问这个问题,实际上是想问切片的底层数组的地址是不是也是连续的
我们知道,切片 slice 在 Go 中是一个结构体,其中 array 字段是一个指针,指向了一块连续的内存地址,也就是底层数组
图片
type slice struct {
array unsafe.Pointer
len int
cap int
}
其中 len 字段记录了当前底层数组的实际有的元素个数,cap 表示底层数组的容量,自然也是切片slice 的容量
func main(){
var demoSli = []int{1,2,3,4,5}
log.Printf("len == %d,cap == %d",len(demoSli),cap(demoSli))
for i, _ := range demoSli {
log.Printf("&demoSli[%d] == %p", i, &demoSli[i])
}
}
图片
自然,demoSli 中的元素打印出来,地址也是连续的,没有毛病
此处 xdm 模拟的时候,切勿去打印拷贝值的地址,例如下面这种方式是相当不明智的
图片
现在简单的去给 切片追加一个元素
图片
可以看到切片的容量变成了原来的两倍(容量从 5 扩容成 10),且切片中底层数组的元素地址自然也是连续的,不需要着急下结论,继续往下看,好戏在后头
切片扩容到底是扩容到原来的几倍?
案例1 向一个cap 为 0 的切片中追加 2000 个元素,查看被扩容了几次
图片
总共是扩容了 14 次
可以看到切片容量小于 1024 时,触发扩容都是扩容到原来的 2 倍,但是 大于 1024 之后,有的是 1.25 倍,有的是 1.35 倍,有的大于 1.35 倍,那么这是为什么呢?后面统一看源码
案例2 再次验证切片容量小于 1024,触发到扩容就一定是扩容 2 倍吗
- 先初始化一个切片,里面有 5 个元素,len 为 5,cap 为 5
- 再向切片中追加 6 个元素,分别是 6,7,8,9,10,11
- 最终查看切片的容量是多少
func main(){
var demoSli = []int{1, 2, 3, 4, 5}
log.Printf("len == %d,cap == %d", len(demoSli), cap(demoSli))
for i, _ := range demoSli {
log.Printf("&demoSli[%d] == %p", i, &demoSli[i])
}
demoSli = append(demoSli,6,7,8,9,10,11)
log.Printf("len == %d,cap == %d",len(demoSli),cap(demoSli))
for i, _ := range demoSli {
log.Printf("&demoSli[%d] == %p", i, &demoSli[i])
}
}
通过这一段代码,我们可以看到,讲一个 len 为 5,cap 为 5 的切片,追加数字 6 的时候,切片应该要扩容到 10,然后追加到数字 11 的时候,切片应该扩容到 20,可实际真的是这样吗?
图片
xdm 可以将上述 demo 贴到自己环境试试,得到的结果仍然会是切片的容量 cap 最终是 12,并不是 20
那么这一切都是为什么呢?我们来查看源码一探究竟
源码赏析
查看公共库中 runtime/slice.go 的 growslice 函数就可以解开我们的疑惑
图片
可以看出在我们使用 append 对切片追加元素的时候,实际上会调用到 growslice 函数, growslice 中的核心逻辑我们就可以理解为计算基本的 newcap 和进行字节对齐
// 省略部分
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.cap < 1024 {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap