1 前言
切片 slice
是 golang
中的一个非常经典的数据结构,它的定位可以类比于其它编程语言中的数组。本篇文章介绍的内容会分为源码实现以及使用 slice
会遇到的问题,走读的源码为 go v1.22.5。
2 源码实现
2.1 数据结构
type slice struct {
array unsafe.Pointer // 指向起点的地址
len int // 切片的长度
cap int // 切片的容量
}
array
:指向了内存地址空间的起点。因为slice
是存放在连续的内存空间上面,当我们需要获取slice
中的值时,可以通过索引index
,在起点的基础上快速进行地址偏移,从而定位到目标元素。len
:切片的长度,指的是slice
逻辑意义上实际存放的长度。cap
:切片的容量,指的是slice
实际物意义上分配了多少个元素的内存空间。
2.2 初始化
func makeslice(et *_type, len, cap int) unsafe.Pointer {
// 通过每个元素占用的内存和 cap 计算出所需要的内存总大小
mem, overflow := math.MulUintptr(et.Size_, uintptr(cap))
// 倘若容量超限,len 取负值或者 len 超过 cap,直接 panic
if overflow || mem > maxAlloc || len < 0 || len > cap {
mem, overflow := math.MulUintptr(et.Size_, uintptr(len))
// 倘若长度超限,len 取负值或者 len 超过 cap,直接 panic
if overflow || mem > maxAlloc || len < 0 {
panicmakeslicelen()
}
panicmakeslicecap()
}
return mallocgc(mem, et, true)
}
上述方法的核心步骤是:
- 通过
math.MulUintptr
方法,根据每个元素的大小以及切片的cap
,计算出初始化切片所需的内存空间大小。 - 如果内存空间超出限制,则直接
panic
。 - 调用
runtime/malloc.go
中的mallocgc
方法进行内存的分配。
2.3 引用传递
首先需要捋清楚引用传递和值传递的两个区别。
引用传递就是将实例对象的地址信息传递到方法内部去,这样在方法里面就可以通过地址追溯到实例所在的位置,因此执行的一些修改操作就会直接影响到原实例。
值传递,指的就是对实例对象进行一次拷贝,得到一个副本,然后将这个副本传递到方法中去,这样在方法里面所修改的只是对这个副本进行操作,不会影响到原实例对象。
在本次聊的 slice
就是属于引用传递:
func changeSlice(s []int) {
s[0] = 0
}
func main() {
s := []int{1, 2, 3}
fmt.Println(s)
changeSlice(s)
fmt.Println(s)
}
代码运行的结果为:
[1 2 3]
[0 2 3]
可以看到,通过方法里面进行修改切片元素,原实例里面的数据也发生了变化。
产生这个结果的原因就在于 slice
的传递是引用传递,并不是值传递,因为每一个切片都对应一个数据结构 slice header
,其中存储了三个字段:
array
:切片内存空间的起始地址 。len
:切片长度。cap
:切片容量。
在每次在方法之间传递切片的时候,会对 slice header
进行一次值拷贝,然后将 slice header
传递到方法中去,但是这个 slice header
副本中的 array
和原 slice
指向同一片内存空间,因此在方法内部进行修改的时候,也会对原切片进行修改。
2.4 切片扩容
发生切片扩容的情况是:当前 slice
的 len
和 cap
的值相同时,下一次 append
操作就会发生一次切片的扩容。
func main() {
s := []int{1, 2, 3}
fmt.Printf("before len %d, cap %d\n", len(s), cap(s))
s = append(s, 4)
fmt.Printf("after len %d, cap %d\n", len(s), cap(s))
}
代码运行的结果为:
before len 3, cap 3
after len 4, cap 6
通过运行结果可以看出,在 len
和 cap
的值相同时,发生了扩容操作。
切片的扩容流程位于 runtime/slice.go
文件下面的 growslice
和 nextslicecap
方法中,其中核心步骤如下:
- 如果新的长度小于 0,直接
panic
。 - 如果切片元素的大小为 0 (元素类型为
struct{}
),则直接复用一个全局的 zerobase 实例,直接返回。 - 如果预期的容量超过原容量的两倍,直接设置新容量为预期容量。
- 如果原容量的大小小于 256 字节,则直接采用原容量的两倍作为新容量。
- 如果原容量的大小已经大于了 256,则在老容量的基础上扩容 1/4 的比例并且累加上 192 的数值,持续这样处理,直到得到的新容量已经大于等于预期的新容量为止。
- 通过
mallocgc
流程,对内存分配单元 mspan 的等级制度,推算得到实际需要申请的内存空间大小。 - 调用
mallocgc
,对新切片进行内存初始化。 - 调用
memmove
方法,将老切片中的内容拷贝到新切片中。 - 返回扩容后的新切片。
func nextslicecap(newLen, oldCap int) int {
// 初始化新容量为旧容量
newcap := oldCap
// 计算两倍旧容量
doublecap := newcap + newcap
// 如果新长度大于两倍旧容量,直接返回新长度
if newLen > doublecap {
return newLen
}
// 定义一个阈值,用于区分小切片和大切片
const threshold = 256
// 如果老容量小于阈值,直接返回两倍的老容量
if oldCap < threshold {
return doublecap
}
// 对于大切片,使用平滑增长的公式
for {
// 平滑增长的公式:新容量增加 (新容量 + 3 * 阈值) 的 1/4
newcap += (newcap + 3*threshold) >> 2
if uint(newcap) >= uint(newLen) {
break
}
}
// 如果新容量计算溢出,设置新容量为新长度
if newcap <= 0 {
return newLen
}
return newcap
}
func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice {
// 计算切片的旧长度,即新长度减去新增元素的数量
oldLen := newLen - num
// ...
if newLen < 0 {
panic(errorString("growslice: len out of range"))
}
if et.Size_ == 0 {
// 倘若元素大小为 0,则无需分配空间直接返回
return slice{unsafe.Pointer(&zerobase), newLen, newLen}
}
// 计算出新的容量
newcap := nextslicecap(newLen, oldCap)
var overflow bool
var lenmem, newlenmem, capmem uintptr
// 判断元素类型是否包含指针
noscan := et.PtrBytes == 0
// 根据元素类型的大小,计算旧长度、新长度和容量的内存大小
switch {
// 如果元素大小为 1 字节
case et.Size_ == 1:
// 计算旧长度的内存大小
lenmem = uintptr(oldLen)
// 计算新长度的内存大小
newlenmem = uintptr(newLen)
capmem = roundupsize(uintptr(newcap), noscan)
// 计算新长度的内存大小
overflow = uintptr(newcap) > maxAlloc
newcap = int(capmem)
// 如果元素大小为指针大小
case et.Size_ == goarch.PtrSize:
// 计算旧长度的内存大小
lenmem = uintptr(oldLen) * goarch.PtrSize
// 计算新长度的内存大小
newlenmem = uintptr(newLen) * goarch.PtrSize
// 计算新容量的内存大小,并向上取整
capmem = roundupsize(uintptr(newcap)*goarch.PtrSize, noscan)
// 检查新容量是否超过最大分配限制
overflow = uintptr(newcap) > maxAlloc/goarch.PtrSize
// 更新新容量
newcap = int(capmem / goarch.PtrSize)
// 如果元素大小是 2 的幂
case isPowerOfTwo(et.Size_):
// 计算元素大小的位移量
var shift uintptr
if goarch.PtrSize == 8 {
// 计算 64 位系统下的位移量
shift = uintptr(sys.TrailingZeros64(uint64(et.Size_))) & 63
} else {
// 计算 32 位系统下的位移量
shift = uintptr(sys.TrailingZeros32(uint32(et.Size_))) & 31
}
// 计算旧长度的内存大小
lenmem = uintptr(oldLen) << shift
// 计算新长度的内存大小
newlenmem = uintptr(newLen) << shift
// 计算新容量的内存大小,并向上取整
capmem = roundupsize(uintptr(newcap)<<shift, noscan)
// 检查新容量是否超过最大分配限制
overflow = uintptr(newcap) > (maxAlloc >> shift)
// 更新新容量
newcap = int(capmem >> shift)
// 重新计算新容量的内存大小
capmem = uintptr(newcap) << shift
default:
// 旧长度的内存大小
lenmem = uintptr(oldLen) * et.Size_
// 新长度的内存大小
newlenmem = uintptr(newLen) * et.Size_
// 新容量的内存大小
capmem, overflow = math.MulUintptr(et.Size_, uintptr(newcap))
// 根据 mallocgc 重新计算容量的内存大小
capmem = roundupsize(capmem, noscan)
// 新的容量大小
newcap = int(capmem / et.Size_)
capmem = uintptr(newcap) * et.Size_
}
// 检查内存空间是否超限
if overflow || capmem > maxAlloc {
panic(errorString("growslice: len out of range"))
}
var p unsafe.Pointer
// 判断是否包含指针
if et.PtrBytes == 0 {
// 分配 capmem 容量的大小,但是不初始化内存,不处理写屏障
p = mallocgc(capmem, nil, false)
memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
} else {
p = mallocgc(capmem, et, true)
if lenmem > 0 && writeBarrier.enabled {
bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(oldPtr), lenmem-et.Size_+et.PtrBytes, et)
}
}
// 将旧数组中的数据复制到新数组中
memmove(p, oldPtr, lenmem)
// 返回新的切片,包含新的底层数组指针、新长度和新容量
return slice{p, newLen, newcap}
}
2.5 切片拷贝
slice
的拷贝分为简单拷贝和完整拷贝两种情况。
如果想要实现简单拷贝,只需要对切片的字面量进行赋值传递即可,这样就相当于创建了一个新的 slice header
的实例,但是其中的指针 array
、容量 cap
和长度 len
仍然和老的 slice header
实例相同。
func main() {
s1 := []int{0, 1, 2, 3, 4}
s2 := s1
fmt.Printf("address of s1: %p, address of s2: %p", s1, s2)
}
显示运行结果
address of s1: 0x1400001c180, address of s2: 0x1400001c180
使用切片的截取操作也是属于简单拷贝,通过下面代码可以看出,s1
和 s2
会使用同一个内存空间,只是地址起点位置偏移了一个元素的长度 s1
和 s2
的地址,刚好差 8 个 byte
。
func main() {
s1 := []int{0, 1, 2, 3, 4}
s2 := s1[1:]
fmt.Printf("address of s1: %p, address of s2: %p", s1, s2)
}
显示运行结果
address of s1: 0x14000128030, address of s2: 0x14000128038
有两种情况会创建完整的复制,也就是创建出一个和 slice
容量大小相等的内存区域,并将原 slice
中的元素一一拷贝到新空间中。
方法一:调用系统 copy
方法,可以看出 s1
和 s2
的地址是相互独立的:
func main() {
s1 := []int{0, 1, 2, 3, 4}
s2 := make([]int, len(s1))
copy(s2, s1)
fmt.Println(s1, s2)
fmt.Printf("address of s1: %p, address of s2: %p", s1, s2)
}
显示运行结果
[0 1 2 3 4] [0 1 2 3 4]
address of s1: 0x1400001c180, address of s2: 0x1400001c1b0
方法二:一开始 s1
赋值给 s2
是浅拷贝,s2
的 len
和 cap
分别为 4、4,当执行 append
操作之后,s2
发生了扩容操作,导致将原来切片中的数据赋值到了新的切片中,s1
和 s2
的地址空间不一样了,修改 s2
也不会导致 s1
中的值发生变化。
func main() {
s1 := []int{0, 1, 2, 3, 4}
s2 := s1[1:]
fmt.Printf("address of s1: %p, address of s2: %p\n", s1, s2)
s2 = append(s2, 5)
fmt.Printf("address of s1: %p, address of s2: %p\n", s1, s2)
fmt.Println(s1, s2)
s2[0] = 10
fmt.Println(s1, s2)
}
显示运行结果
address of s1: 0x14000116030, address of s2: 0x14000116038
address of s1: 0x14000116030, address of s2: 0x14000120040
[0 1 2 3 4] [1 2 3 4 5]
[0 1 2 3 4] [10 2 3 4 5]
3 问题
3.1 问题1
- 初始化切片 s 长度和容量均为 10。
- 在 s 的基础上追加 append 一个元素。
请问经过上述操作后,切片s 的内容、长度以及容量分别是什么?
func main() {
s := make([]int, 10)
s = append(s, 10)
fmt.Printf("s: %v, len of s: %d, cap of s: %d", s, len(s), cap(s))
}
显示运行结果
s: [0 0 0 0 0 0 0 0 0 0 10], len of s: 11, cap of s: 20
3.2 问题2
- 初始化切片 s 长度为 0,容量为 10。
- 在 s 的基础上追加 append 一个元素。
请问经过上述操作后,切片s 的内容、长度以及容量分别是什么?
func main() {
s := make([]int, 0, 10)
s = append(s, 10)
fmt.Printf("s: %v, len of s: %d, cap of s: %d", s, len(s), cap(s))
}
显示运行结果
s: [10], len of s: 1, cap of s: 10
3.3 问题3
- 初始化切片 s 长度为 10,容量为 11。
- 在 s 的基础上追加 append 一个元素。
请问经过上述操作后,切片s 的内容、长度以及容量分别是什么?
func main() {
s := make([]int, 10, 11)
s = append(s, 10)
fmt.Printf("s: %v, len of s: %d, cap of s: %d", s, len(s), cap(s))
}
显示运行结果
s: [0 0 0 0 0 0 0 0 0 0 10], len of s: 11, cap of s: 11
3.4 问题4
- 初始化切片 s 长度为 10,容量为 12
- 截取切片 s index = 8 往后的内容赋给 s1
请问 s1 的内容、长度以及容量分别是什么?
func main() {
s := make([]int, 10, 12)
s1 := s[8:]
fmt.Printf("s1: %v, len of s1: %d, cap of s1: %d", s1, len(s1), cap(s1))
}
显示运行结果
s1: [0 0], len of s1: 2, cap of s1: 4
3.5 问题5
- 初始化切片 s 长度为 10,容量为 12。
- 截取切片 s index 为 [8,9) 范围内的元素赋给切片 s1。
请问 s1 的内容、长度以及容量分别是什么?
func main() {
s := make([]int, 10, 12)
s1 := s[8:9]
fmt.Printf("s1: %v, len of s1: %d, cap of s1: %d", s1, len(s1), cap(s1))
}
显示运行结果
s1: [0], len of s1: 1, cap of s1: 4
3.6 问题6
- 初始化切片 s 长度为 10,容量为 12。
- 截取切片 s index = 8 往后的内容赋给 s1。
- 修改 s1[0] 的值。
请问这个修改是否会影响到 s? 此时,s 的内容是什么?
func main() {
s := make([]int, 10, 12)
s1 := s[8:]
s1[0] = -1
fmt.Printf("s: %v", s)
}
显示运行结果
s: [0 0 0 0 0 0 0 0 -1 0]
3.7 问题7
- 初始化切片 s 长度为 10,容量为 12
请问,访问 s[10] 是否会越界?
func main() {
s := make([]int, 10, 12)
v := s[10]
fmt.Printf("v: %d", v)
}
显示运行结果
panic: runtime error: index out of range [10] with length 10
3.8 问题8
- 初始化切片 s 长度为 10,容量为 12。
- 截取 s 中 index = 8 后面的内容赋给 s1。
- 在 s1 的基础上追加 []int{10,11,12} 3 个元素。
请问,经过上述操作时候,访问 s[10] 是否会越界?
func main() {
s := make([]int, 10, 12)
s1 := s[8:]
s1 = append(s1, []int{10, 11, 12}...)
v := s[10]
fmt.Printf("v: %d", v)
}
显示运行结果
panic: runtime error: index out of range [10] with length 10
3.9 问题9
- 初始化切片 s 长度为 10,容量为 12。
- 截取切片 s index = 8 往后的内容赋给 s1。
- 在方法 changeSlice 中,对 s1[0] 进行修改。
求问,经过上述操作之后,s 的内容是什么?
func changeSlice(s1 []int) {
s1[0] = -1
}
func main() {
s := make([]int, 10, 12)
s1 := s[8:]
changeSlice(s1)
fmt.Printf("s: %v", s)
}
显示运行结果
s: [0 0 0 0 0 0 0 0 -1 0]
3.10 问题10
- 初始化切片 s 长度为 10,容量为 12。
- 截取切片 s index = 8 往后的内容赋给 s1。
- 在方法 changeSlice 中,对 s1 进行 apend 追加操作。
请问,经过上述操作后,s 以及 s1 的内容、长度和容量分别是什么?
func changeSlice(s1 []int) {
s1 = append(s1, 10)
}
func main() {
s := make([]int, 10, 12)
s1 := s[8:]
changeSlice(s1)
fmt.Printf("s: %v, len of s: %d, cap of s: %d\n", s, len(s), cap(s))
fmt.Printf("s1: %v, len of s1: %d, cap of s1: %d\n", s1, len(s1), cap(s1))
}
显示运行结果
s: [0 0 0 0 0 0 0 0 0 0], len of s: 10, cap of s: 12
s1: [0 0], len of s1: 2, cap of s1: 4
3.11 问题11
- 初始化切片 s,内容为 []int{0,1,2,3,4}。
- 截取 s 中 index = 2 前面的内容(不含s[2]),并在此基础上追加 index = 3 后面的内容。
请问,经过上述操作后,s 的内容、长度和内容分别是什么?此时访问 s[4] 是否会越界?
func main() {
s := []int{0, 1, 2, 3, 4}
s = append(s[:2], s[3:]...)
fmt.Printf("s: %v, len: %d, cap: %d\n", s, len(s), cap(s))
v := s[4]
fmt.Printf("v: %v\n", v)
}
显示运行结果
s: [0 1 3 4], len: 4, cap: 5
panic: runtime error: index out of range [4] with length 4
3.12 问题12
- 初始化切片 s 长度和容量均为 512。
- 在 s 的基础上追加 append 一个元素。
请问经过上述操作后,切片s 的内容、长度以及容量分别是什么?
func main() {
s := make([]int, 512)
s = append(s, 1)
fmt.Printf("len of s: %d, cap of s: %d", len(s), cap(s))
}
显示运行结果
len of s: 513, cap of s: 848
4 总结
slice
是一个长度可变的连续数据序列,在实现上基于一个slice header
组成,其中包含的字段包括:指向内存空间地址起点的指针array
、一个表示了存储数据长度的len
和分配空间长度的cap
。- 由于
slice
在传递过程中,本质上传递的是slice header
实例中的内存地址array
,因此属于引用传递。 slice
在扩容时,遵循如下机制:- 如果扩容时预期的新容量超过原容量的两倍,直接取预期的新容量。
- 如果原容量小于 256,直接取原容量的两倍作为新容量。
- 如果原容量大于等于 256,在原容量
n
的基础上循环执行n += (n+3*256)/4
的操作,直到n
大于等于预期新容量,并取n
作为新容量。
- 当执行
copy
系统调用或者副本的slice
发生了扩容的操作时,会将副本的slice
拷贝到新的切片中。
5 参考
- 小徐先生的编程世界
- Golang 源码 1.22.5