本篇go version 1.18
Go slice
slice的存储结构
Go中的slice依赖于数组,具备数组所有的优点。
slice结构:先创建一个有特定长度和数据类型的底层数组,然后从这个底层数组中选取一部分元素,返回这些元素组成的集合,并将slice指向集合中的第一个元素。换句话说,slice自身维护了一个指针属性,指向它底层数组中的某些元素的集合。
例如,初始化一个slice数据结构:
func main() {
test := make([]int, 3, 6)
fmt.Println(test)
}
上述程序输出:
[0 0 0]
过程解析:
- 声明长度为6,数据类型为int的数组
- 取前3个元素,取index0-2元素
- slice struct指针指向底层数组的第一个元素,此处指向index为0的元素地址
底层数据结构:
源码路径:src/runtime/slice.go
type slice struct {
array unsafe.Pointer
len int
cap int
}
slice struct中有3个成员:
- array:底层数组指针,指向底层分配的数组开始的元素地址;
- len:slice当前的长度;
- cap:底层数组的长度;
创建、初始化、获取元素
创建 、初始化
创建slice数据结构的方式
1:make()
// 创建len和cap都为6的slice
s1 := make([]int, 6)
fmt.Println(s1)
// 创建len为3、cap为6的slice
s2 := make([]int, 3, 6)
fmt.Println(s2)
make关键字:可以为底层数组分配内存,并从底层数组中生成一个slice并初始化。
make可以构建slice、map、channel三种数据对象,先为底层数据结构分配内存并初始化。
2: 直接赋值初始化
// 创建长度和容量为4的切片,并初始化
s3 := []int{1, 2, 3, 4, 5, 6}
fmt.Println(s3)
获取slice中元素
slice只能访问len范围内的元素,而在len之外,cap之内的元素不能被访问,panic如下代码段
// 创建len为3、cap为6的slice,获取第4个元素
s2 := make([]int, 3, 6)
fmt.Println(s2[2]) // 0
fmt.Println(s2[3]) // panic: runtime error: index out of range [3] with length 3
nil slice和Empty slice
nil slice
只声明一个slice,不做初始化时,此时是nil slice,不会指向任何数组,此时len和ca p都为0
// 声明一个nil slice
var nils []int
type slice struct {
array unsafe.Pointer // nil
len int // 0
cap int // 0
}
empty slice
空slice是指底层数组指向的是长度为0的空数组。
// 声明一个空slice
emptys1 := make([]int,0)
emptys2 := []int{}
type slice struct {
array unsafe.Pointer // 地址
len int // 0
cap int // 0
}
比较
虽然nil slice和Empty slice的长度和容量都为0,输出时的结果都是[],且都不存储任何数据,但它们是不同的。nil slice不会指向底层数组,而空slice会指向底层数组,只不过这个底层数组暂时是空数组。
var nils []int
emptys := []int{}
println(nils) // [0/0]0x0
println(emptys) // [0/0]0x14000062ef8
但是,无论是nil slice还是empty slice,都可以对它们进行操作,如append()函数、len()函数和cap()函数。
fmt.Println(len(nils)) // 0
fmt.Println(len(emptys)) // 0
fmt.Println(cap(nils)) // 0
fmt.Println(cap(emptys)) // 0
nils = append(nils, 1)
emptys = append(emptys, 1)
fmt.Println(nils) // [1]
fmt.Println(emptys) // [1]
切片
切片语法:
SLICE[A:B]
SLICE[A:B:C]
其中A表示从SLICE的第几个元素开始切,B控制切片的长度(B-A),C控制切片的容量(C-A),如果没有给定C,则表示切到底层数组的最尾部。
SLICE[A:] // 从A切到最尾部
SLICE[:B] // 从最开头切到B(不包含B)
SLICE[:] // 从头切到尾,等价于复制整个SLICE
注意:截取时“左闭右开”
多个slice可能共享同一个底层数组,所以当修改了某个slice中的元素时,其他包含该元素的slice也会随之改变。
copy()函数
copy()可以将一个slice copy到另一个slice中。
$ go doc builtin copy
func copy(dst, src []Type) int
表示将src slice拷贝到dst slice,src比dst长,就截断,src比dst短,则只拷贝src那部分。
copy的返回值是拷贝成功的元素数量,所以也就是src slice或dst slice中最小的那个长度。
此外,copy还能将字符串拷贝到byte slice中,因为字符串实际上就是[]byte。
append()函数
当slice的length已经等于capacity的时候,再使用append()给slice追加元素,会触发扩容操作。
slice扩容操作,底层会生成一个新的数组,并将原来的底层数组的元素拷贝至新数组中,并返回新的slice。
换句话说,旧的底层数组还会被旧的slice引用,因此此时,新slice和旧slice不再共享同一个底层数组。
如下代码段:
var arr = []int{}
for i := 0; i < 10; i++ {
arr = append(arr, i)
println(arr)
}
fmt.Println(arr)
输出结果:
[1/1]0x14000120008
[2/2]0x14000120010
[3/4]0x14000122000
[4/4]0x14000122000
[5/8]0x14000124000
[6/8]0x14000124000
[7/8]0x14000124000
[8/8]0x14000124000
[9/16]0x14000126000
[10/16]0x14000126000
[0 1 2 3 4 5 6 7 8 9]
上面说slice扩容时底层会生成一个新的数组,因此每次append触发扩容后,都会生成一个新的底层数组。
合并slice
append(s1,s2…)表示将s2合并在s1的后面,并返回新的slice。
注意:append()最多允许两个参数,所以一次性只能合并两个slice。但可以取巧,将append()作为另一个append()的参数,从而实现多级合并。例如,下面的合并s1和s2,然后再和s3合并,得到s1+s2+s3合并后的结果。
sn := append(append(s1,s2...),s3...)
切片/copy()/append()/合并slice
slice的操作(切片/copy()/append()/合并slice)都会产生新的slice,但新老slice在扩容时刻是共用底层数组的。
未触发扩容操作:新老slice增删改元素都会影响到原slice底层数组;
触发扩容操作:底层数组扩容会生成一个新的底层数组并且被新的slice指向,此时和老的slice完全没有关系,老的slice增删改元素完全不会影响新的slice。
slice遍历
range关键字可以对slice进行迭代,每次返回一个index和对应的元素值。可以将range的迭代结合for循环对slice进行遍历。
s1 := []int{1,2,3,4,5,6}
for index,value := range s1 {
println("index:",index," , ","value",value)
}
传递slice给函数
slice实际上包含了3个属性,它的数据结构类似于[3/5]0xc42003df10。
Go中函数的参数是按值传递的,所以调用函数时会复制一个参数的副本传递给函数,如果传递的是slice,它将复制该slice副本给函数,这个副本其实就是[3/5]0xc42003df10,所以传递给函数的副本仍然指向源slice的底层数组。
因此,如果函数内部对slice进行了修改,有可能会直接影响函数外部的底层数组,从而影响其他slice。但并不完全是这样,例如函数内部对slice进行扩容,扩容时生成了一个新的底层数组,函数后续的代码支队新的底层数组操作,这样就不会影响原始的底层数组。
未触发扩容:
func main() {
var s = make([]int, 2, 4)
s[0] = 1
println(s)
fmt.Println("pre method", s)
testSliceNoExpand(s)
println(s)
fmt.Println("post method", s)
}
func testSliceNoExpand(s []int) {
s = append(s, 3)
println(s)
}
输出:
[2/4]0x140000aa000
pre method [1 0]
[4/4]0x140000aa000
in method [1 0 3 3]
[2/4]0x140000aa000
post method [1 0]
触发扩容:
func main() {
var s = make([]int, 2, 4)
s[0] = 1
println(s)
fmt.Println("pre method", s)
testSliceExpand(s)
println(s)
fmt.Println("post method", s)
}
func testSliceExpand(s []int) {
s = append(s, 3)
s = append(s, 4)
s = append(s, 5)
println(s)
fmt.Println("in method", s)
}
输出:
[2/4]0x1400012c000
pre method [1 0]
[5/8]0x14000130000
in method [1 0 3 4 5]
[2/4]0x1400012c000
post method [1 0]
slice和内存浪费问题
由于slice的底层是数组,很可能数组很大,但slice所取的元素数量却很小,这就导致数组占用的绝大多数空间是被浪费的。
特别地,垃圾回收器(GC)不会回收正在被引用的对象,当一个函数直接返回指向底层数组的slice时,这个底层数组将不会随函数退出而被回收,而是因为slice的引用而永远保留,除非返回的slice也消失。
因此,当函数的返回值是一个指向底层数组的数据结构时(如slice),应当在函数内部将slice拷贝一份保存到一个使用自己底层数组的新slice中,并返回这个新的slice。这样函数一退出,原来那个体积较大的底层数组就会被回收,保留在内存中的是小的slice。
本篇参考:Go基础系列:Go slice详解