Golang Slice 结构体
写这个的初衷是因为看到一个 b站的UP主(幼麟实验室)做 Golang 的视频有感而生,想通过视频深入剖析一下内容,顺便当作一个输出,记录自己学习的过程。
up主的连接如下:幼麟实验室
Slice 结构体
在 Golang 里面,Slice
的结构体如下所示:
第一个成员为指向底层数组的指针,第二个为该切片的长度,第三个为底层数组的容量。简单的解释就是,len
为这个Slice
结构体可以获取数据的最大位置,而cap
为指向的底层数组的实际容量。以下为slice
的结构体定义:
// runtime/slice.go
type slice struct {
array unsafe.Pointer
len int
cap int
}
通过下面的方式来定义一个整数切片
var ints []int
此时,ints
切片的data
部分为nil
,长度为0
,容量为0
。具体结构如[nil, 0, 0]
。如果此时使用make
为ints
开辟一个空间,如下图所示:
ints = make([]int, 2) // len = 2
make
第一个参数为数组类型,第二个为切片长度。但是如果此时加入第三个参数,如下所示:
ints = make([]int, 2,5) // len = 2. cap = 5
第三个参数为切片的容量。所以,用户是可以自定义该切片的容量的。你们也可以通过如下实验来检测这个的正确性。
import "fmt"
func main() {
var ints []int
ints = make([]int, 2)
fmt.Println(" len: ", len(ints), " cap: ", cap(ints))
ints = make([]int, 2, 5)
fmt.Println(" len: ", len(ints), " cap: ", cap(ints))
}
output
>>> len: 2 cap: 2
>>> len: 2 cap: 5
刚刚已经知道,len
为切片的最大可访问位置,如果我们执行以下代码,就会发生panic
。
func main() {
var ints []int
ints = make([]int, 2, 5)
fmt.Println("len:", len(ints), "cap:", cap(ints))
fmt.Println(ints[2])
}
output
>>> len: 2 cap: 2
>>> panic: runtime error: index out of range [2] with length 2
如果上述ints
变量我们使用 new 来进行初始化呢,操作如下:
ints := new([]int)
此时,ints为一个指针,指向的是 slice 结构的起始地址,效果如下所示:
此时,ints
指向一个data
为nil
的空的切片。此时,如果想要为ints
指向的切片开辟地址空间,可以使用append
方法。如下:
ints = append(ints, 1)
此时,ints
指向的切片就会变成[data, 1, 1]
,data
为指向底层数组的指针,长度为1
,容量为1
。通过如下实验可以证明这个结果:
func main() {
ints := new([]int)
fmt.Println(&ints)
fmt.Println("len:", len(*ints), "cap:", cap(*ints))
*ints = append(*ints, 1)
fmt.Println("len:", len(*ints), "cap:", cap(*ints))
}
output
>>> 0xc0000ce018
>>> len: 0 cap: 0
>>> len: 1 cap: 1
第一个输出可以看出,ints
是一个指针,此时通过指针访问切片可以知道,该切片长度为0
,容量为0
。通过append
添加元素之后,切片长度变为1
,容量也为1
。
Slice 结构体获取“子”切片
使用过 python 的都知道,python 数组可以直接通过索引来获得子数组,如下所示:
nums = [0,1,2,3,4]
sub = nums[1:3]
print(sub)
output
>>> [1,2]
golang 中,切片也可以通过如下的方式来获取子数组。
ints := [10]int{1,2,3,4,5,6,7,8,9,10}
var s1 = ints[1:4]
var s2 = ints[7:]
通过这个方法得到的s1
,s2
切片的底层数组指向的是ints
数组。
在生成切片时,切片的长度为我们定义的长度[1:4]
即为3
,而切片的指针指向目标的起始位置。s1
为arr
从索引1
到索引3
的数据,所以data
指向arr[1]
的地址,而容量则为起始位置到该底层数组的结束位置的大小,即[1-9]
总共为9
。s2
也同理。通过如下的实验可以验证这个场景。
func main() {
arr := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
for i := 0; i < 10; i++ {
println(&arr[i])
}
println("=====================")
var s1 []int = arr[1:4]
var s2 []int = arr[7:]
a1 := &s1[0]
println(a1)
a2 := &s2[0]
println(a2)
}
output
>>> 0xc000077f20
>>> 0xc000077f28
>>> 0xc000077f30
>>> 0xc000077f38
>>> 0xc000077f40
>>> 0xc000077f48
>>> 0xc000077f50
>>> 0xc000077f58
>>> 0xc000077f60
>>> 0xc000077f68
>>> =====================
>>> 0xc000077f28
>>> 0xc000077f58
本次使用的电脑为64
位的,所以int
的大小为8
个字节。可以看出,arr
数组的各个元素之间的地址确实是差8
个字节。分割线之后的第一个为s1
底层数组指向的位置,0xc000077f28
为arr[1]
的地址,而0xc000077f58
为arr[7]
的位置。
重点
如果此时对s1
切片进行扩容,s1
的切片结构就会从[data, 3, 9]
变成[data, 4, 9]
。而此时,s1
的指针依旧还是在arr[1]
的位置上,但是此时append
添加的元素就会修改在arr
数组上。代码如下:
func main() {
arr := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
fmt.Println(arr)
println("=====================")
var s1 []int = arr[1:4]
fmt.Println(&s1[0], " len: ", len(s1), " cap: ", cap(s1))
s1 = append(s1, 1)
fmt.Println(&s1[0], " len: ", len(s1), " cap: ", cap(s1))
println("=====================")
fmt.Println(arr)
}
output
>>> [0 1 2 3 4 5 6 7 8 9]
>>> =====================
>>> 0xc0000b00a8 len: 3 cap: 9
>>> 0xc0000b00a8 len: 4 cap: 9
>>> =====================
>>> [0 1 2 3 1 5 6 7 8 9]
可以看出,扩容前扩容后的数组指向的都为arr[1]
的地址位置,但是此时如果对s1
进行扩容,因为扩容后长度仍小于容量,所以slice
不会重新创建一个新的数组,而此时扩容放入的1
就被赋值在了arr[4]
这个位置上。此时就会对原数组数据进行修改,造成数据不安全。但是,如果扩容后的长度大于容量的话,slice
就会创建一个新的数组,并将值赋值到新的底层数组上。
func main() {
arr := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
fmt.Println(arr)
println("=====================")
var s1 []int = arr[7:]
fmt.Println(&s1[0], " len: ", len(s1), " cap: ", cap(s1))
s1 = append(s1, 1)
fmt.Println(&s1[0], " len: ", len(s1), " cap: ", cap(s1))
println("=====================")
fmt.Println(arr)
println("=====================")
fmt.Println(s1)
}
output
>>> [0 1 2 3 4 5 6 7 8 9]
>>> =====================
>>> 0xc0000b00d8 len: 3 cap: 9
>>> 0xc0000c0090 len: 4 cap: 6
>>> =====================
>>> [0 1 2 3 4 5 6 7 8 9]
>>> =====================
>>> [7 8 9 1]
可以看出,此时扩容前后数组的底层数组位置发生了变化,且增加不再影响原来的数组了。
Slice 结构体扩容机制
从上文最后一个实验可以看出,s2
在扩容之后,容量不是从3 -> 4
,而是变成了6
。那么,slice
是如何预估扩容之后的容量呢。
slice
结构体的扩容代码如下:
// 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.cap < 1024 {
newcap = doublecap
} else {
//...
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
}
}
//...
}
简而言之,如果newcap > 2 * oldcap
(注意,此时这个newcap
和上文代码中的newcap
表达不一致,本文的newcap
就是新容量的意思),那么直接将容量设置为新容量。举下面这个例子:
ints := make([]int, 2)
ints = append(ints, 2, 3, 4)
原来的容量为2
,新容量为5
,因为2 * 2 < 5
,所以此时新的容量为5
。
否则,如果oldcap < 1024
,则直接将容量翻倍,如果大于1024
则放大1.25
倍。
short_ints := make([]int, 2)
short_ints = append(ints, 2) // 这个为翻倍
long_ints := make([]int, 2048)
long_ints = append(ints, 2) // 这个为 1.25 倍
但是可能会问了,上文的实验中,最后的实验产生的s2
容量为6
,并不是5
。这个是因为,golang 在分配内存空间的时候,不会按照需要的大小去给对应的大小,而是会从内存管理模块中获取一个足够大且大小最相近的内存。因为,golang 在运行时,会预先向系统申请一部分内存,然后按照不同大小,分类缓存起来,等到需要的时候申请即可。此时不对此内容进行深究,需要的话可以看这个博主的博文。博文地址
在扩容时,申请的内存大小为新容量 * 类型大小
,在如下这个实验中:
ints := make([]int, 2)
ints = append(ints, 2, 3, 4)
新的容量为5
,类型大小为8
个字节,所以所需的内存大小为40
个字节。而实际申请时,会匹配到48
字节的大小,48
个字节正好是容量为6
的int
数组的大小,所以显示出来容量为6
。通过如下实验获取结果:
func main() {
ints := make([]int, 2)
fmt.Println(&ints[0], " len: ", len(ints), " cap: ", cap(ints))
ints = append(ints, 2, 3, 4)
fmt.Println(&ints[0], " len: ", len(ints), " cap: ", cap(ints))
println("=====================")
ints2 := make([]int, 2)
fmt.Println(&ints2[0], " len: ", len(ints2), " cap: ", cap(ints2))
ints2 = append(ints2, 2)
fmt.Println(&ints2[0], " len: ", len(ints2), " cap: ", cap(ints2))
println("=====================")
ints3 := make([]int, 1024)
fmt.Println(&ints3[0], " len: ", len(ints3), " cap: ", cap(ints3))
ints3 = append(ints3, 2)
fmt.Println(&ints3[0], " len: ", len(ints3), " cap: ", cap(ints3))
}
output
>>> =====================
>>> 0xc0000180a0 len: 2 cap: 2
>>> 0xc00000a360 len: 5 cap: 6
>>> =====================
>>> 0xc0000180d0 len: 2 cap: 2
>>> 0xc00000e200 len: 3 cap: 4
>>> =====================
>>> 0xc00007c000 len: 1024 cap: 1024
>>> 0xc000100000 len: 1025 cap: 1280
从上面的第二个输出可以看出,容量满足翻倍的条件,进行了翻倍,同时匹配到的内存规格为32
字节的,所以此时容量为4
。而容量大于等于1024
,则增长了1.25
倍,且内存规格为1280
(1280
也正好是1024
的1.25
倍)。
但是可以发现ints2
和ints
的内存地址是相同的,不知道是不是 golang 对这个做了些优化,目前我还不清楚,如果有知道的朋友也请告诉我。
最后
不得不说,前面提到的 UP 主做的视频真的很适合 golang 开发者,但是可能需要有一定理解再去看会比较有收获。还有就是,打博文太难了…