Golang - 深入解析slice 扩容底层实现
1、切片的定义
切片就是一种简化版的动态数组,长度不固定,便于使用和管理数据集合。
切片是对数组一个连续片段的引用,所以切片是一个引用类型。
2、切片的内部结构
type SliceHeader struct { Data uintptr Len int Cap int }
内部实现的数据结构通过指针引用底层数组,设定相关属性将数据读写操作限定在指定区域内。
1)指针,指向底层数组中切片指定的开始位置,一块连续的内存;
2)长度,即元素的数量;
3)容量,即数组的最大长度,也就是切片开始位置到数组的最后位置的长度。
3、切片的基础操作
1)、 定义
var s []int // var identifier []type
var numbers = make([]int, 3, 5) //var numbers = make([]T, length, capacity)
2)、初始化
A:直接初始化
s := []int {1,2,3 }
s :=make([]int,len,cap)
B:通过数组截取初始化 arr := [5]int {1,2,3,4,5}
a)s := arr[:]
切⽚中包含数组的所有元素;
b)s := arr[startIndex:endIndex]
将arr中从下标startIndex到endIndex-1 下的元素创建为⼀个新的切⽚(前闭后开),⻓度为endIndex-startIndex;
c)s := arr[startIndex:]
索引在冒号左边,表示截取索引startInex左边的所有元素,即元素范围:arr[0]~arr[strartIndex-1],一共有startIndex个元素;
d)s := arr[:endIndex]
索引在冒号右边,表示截取索引endInex右边的所有元素,即元素范围:arr[endIndex]~arr[len(arr) - 1], 一共有len(arr) - endIndex个元素
3)、删除
要删除元素的下标index,比较取巧的方法是:
要从切片
a
中删除索引为index
的元素,操作方法是a = append(a[:index], a[index+1:]...)
var s1 = []int{1, 2, 3, 4} // 初始化一个切片
var index = 2 // 要删除的下标
s1 = append(s1[:index], s1[index+1:]...) // 删除下标为index的元素
说明:s1[:index]
指的是[0, index)区间的元素,注意是左闭右开,s1[index+1:]
指的是[index+1, len)区间的元素,使用append将这两个区间合并后,刚好把index下标的元素干掉。
注意分隔操作符" :"的使用,有以下几个特点:
i) 分隔符:可以对数组或者slice做数据截取,:得到的结果是一个新slice。
ii) 新slice结构体里的array指针指向原数组或者原slice的底层数组,新切片的长度是:右边的数值减去左边的数值,新切片的容量是原切片的容量减去:左边的数值。
iii) 分隔符:的左边如果没有写数字,默认是0,右边没有写数字,默认是被分割的数组或被分割的切片的长度。
a := make([]int, 0, 4) // a的长度是0,容量是4
b := a[:] // 等价于 b := a[0:0], b的长度是0,容量是4
c := a[:1] // 等价于 c := a[0:1], b的长度是1,容量是4
d := a[1:] // 编译报错 panic: runtime error: slice bounds out of range
e := a[1:4] // [0 0 0],e的长度3,容量3
iiii) 分割操作符: 右边的数值有上限,上限有2种情况
如果分割的是数组,那上限是是被分割的数组的长度。
如果分割的是切片,那上限是被分割的切片的容量。注意,这个和下标操作不一样,如果使用下标索引访问切片,下标索引的最大值是(切片的长度-1),而不是切片的容量。
iiiii) 对数组或者切片做:
分割操作产生的新切片还是指向原来的底层数组,并不会把原底层数组的元素拷贝一份到新的内存空间里。
4)、空切片
一个切片在未初始化之前默认为 nil,长度为 0
4、切片的扩容
1)、追加
var s1 = []int{1, 2} // 初始化一个切片
s1 = append(s1, 3) // 在最后添加一个元素
遵循两个原则:
A: append函数调用后,应该使用返回值作为结果。
B: append函数调用后,不应该再使用实参传入的slice。
使用append函数一般都是s = append(s,elem1)
这种用法,也就是把结果重新赋值给原来的slice。
2)、扩容机制
结论:
- 当需要的容量大于原切片容量的两倍时,会使用需要的容量作为新容量。
- 当原切片长度小于1024时,新切片的容量会直接翻倍。而当原切片的容量大于等于1024时,会反复地增加25%,直到新容量超过所需要的容量。
3)、示例(查看这里)
4)、解读源码
// src/runtime/slice.go
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 {
// 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 <= 0 {
newcap = cap
}
}
}
// ...省略部分
}
4)、扩容后数组的指向
A: 若原切片还有容量可以扩容,执行append操作是对原有切片进行,所以这种情况下,扩容以后的数组还是指向原来的数组,扩容后的数组中某一个值的修改会影响原切片与原数组
B: 向原有切片新增一个元素生成新切片时,若原切片已满,Go 默认会先开一片内存区域,把原来的值拷贝过来,然后再执行 append() 操作,这种情况不影响原数组
5、切片的复制(copy)
将一个数组切片复制到另一个数组切片中,如果加入的两个数组切片不一样大,就会按照其中较小的那个数组或切片的元素个数进行复制。
copy( dstSlice, srcSlice []T) int
copy
会从原切片srcSlice
拷贝 min(len(dstSlice), len(srcSlice))
个元素到目标切片dstSlice
因为拷贝的元素个数min(len(dst), len(src))
不会超过目标切片的长度len(dst)
,所以copy
执行后,目标切片的长度不会变,容量不会变,只改变元素内容。
slice1 := []int{1, 2, 3, 4, 5}
slice2 := []int{5, 4, 3}
copy(slice2, slice1) //slice2: [1 2 3] 只会复制slice1的前3个元素到slice2中
copy(slice1, slice2) //slice1: [5 4 3 4 5] 只会复制slice2的3个元素到slice1的前3个位置
6、总结
Golang中的切片扩容机制,与切片的数据类型、原本切片的容量、所需要的容量都有关系,比较复杂。对于常见数据类型,在元素数量较少时,大致可以认为扩容是按照翻倍进行的,但具体情况需要具体分析。
为了避免因为切片是否发生扩容的问题导致bug,最好的处理办法还是在必要时使用
copy
来复制数据,保证得到一个新的切片,以避免后续操作带来预料之外的副作用。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· DeepSeek 开源周回顾「GitHub 热点速览」