不会飞的章鱼

熟能生巧,勤能补拙;念念不忘,必有回响。

Go语言精进——了解切片原理并高效使用

什么是数组

数组有哪些基本特性

Go 语言的数组是一个长度固定的、由同构类型元素组成的连续序列。
因此Go 的数组类型包含两个重要属性:元素的类型和数组长度(元素的个数)。
所以,Go 语言中数组类型变量的声明:

1
var arr [N]T //声明了一个数组变量 arr,它的类型为[N]T,其中元素的类型为 T,数组的长度为N。

通过声明,我们可以得出一个结论:如果两个数组类型的元素类型 T 与数组长度 N 都是一样的,那么这两个数组类型是等价的,如果有一个属性不同,它们就是两个不同的数组类型。

数组类型不仅是逻辑上的连续序列,而且在实际内存分配时也占据着一整块内存。
Go 编译器在为数组类型的变量实际分配内存时,会为 Go 数组分配一整块、可以容纳它所有元素的连续内存,如下图所示:

我们从这个数组类型的内存表示中可以看出来,这块内存全部空间都被用来表示数组元素,所以说这块内存的大小,就等于各个数组元素的大小之和。如果两个数组所分配的内存大小不同,那么它们肯定是不同的数组类型。

Go 提供了预定义函数 len 可以用于获取一个数组类型变量的长度,通过 unsafe 包提供的 Sizeof 函数,我们可以获得一个数组变量的总大小。

多维数组怎么解?

例如:

1
var mArr [2][3][4]int

什么是切片

数组作为最基本同构类型在 Go 语言中被保留了下来,但数组在使用上确有两点不足:固定的元素个数,以及传值机制下导致的开销较大。于是,引入了另外一种同构复合类型:切片(slice),来弥补数组的这两处不足。

声明并初始化一个切片变量:

1
var nums = []int{1,2,3,4,5,6}

与数组声明相比,切片声明仅仅是少了一个“长度”属性。去掉“长度”这一束缚后,切片展现出更为灵活的特性。

虽然切片变量缺少了“长度”属性,但不代表它没有长度,而是长度一直在变化。

我们可以通过 len 函数获得切片类型变量的长度,通过 Go 内置函数 append,我们可以动态地向切片中添加元素。

1
2
3
fmt.Println(len(nums)) // 6
nums = append(nums,7) // [1,2,3,4,5,6,7]
fmt.Println(len(nums)) // 7

Go是如何实现切片类型的

Go 切片在运行时其实是一个三元组结构,它在 Go 运行时中的表示如下:

1
2
3
4
5
type slice struct {
array unsafe.Pointer //指向底层数组的指针
len int //是切片的长度,即切片中当前元素的个数
cap int //是底层数组的长度,也是切片的最大容量,cap 值永远大于等于 len 值
}

Go 编译器会自动为每个新创建的切片,建立一个底层数组,默认底层数组的长度与切片初始元素个数相同。

通过 make 函数来创建切片,并指定底层数组的长度

1
sl := make([]byte,6,10)  //其中10为cap值,即底层数组长度,6为切片的初始长度

如果没有在 make 中指定 cap 参数,那么底层数组长度 cap 就等于 len,比如:

1
sl := make([]byte,6)  // cap = len = 6

采用 array[low : high : max]语法基于一个已存在的数组创建切片

这种方式被称为数组的切片化。

1
2
arr := [10]int{1,2,3,4,5,6,7,8,9,10}
sl := arr[3:7:9]

通过类比,发现切片好比打开了一个访问与修改数组的“窗口”,通过这个窗口,我们可以直接操作底层数组中的部分元素。

在 Go 语言中,数组更多是“退居幕后”,承担的是底层存储空间的角色。切片就是数组的“描述符”,也正是因为这一特性,切片才能在函数参数传递时避免较大性能开销。

另外,针对一个已存在的数组,我们还可以建立多个操作数组的切片,这些切片共享同一底层数组,切片对底层数组的操作也同样会反映到其他切片中。

基于切片创建切片

切片的动态扩容

“动态扩容”指的就是,当我们通过 append 操作向切片追加数据的时候,如果这时切片的 len 值和 cap 值是相等的,也就是说切片底层数组已经没有空闲空间再来存储追加的值了,Go 运行时就会对这个切片做扩容操作,来保证切片始终能存储下追加的新值。
例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
var s []int
s = append(s, 1)
fmt.Println(len(s), cap(s)) //1 1
s = append(s, 2)
fmt.Println(len(s), cap(s)) //2 2
s = append(s, 3)
fmt.Println(len(s), cap(s)) //3 4
s = append(s, 4)
fmt.Println(len(s), cap(s)) //4 4
s = append(s, 5)
fmt.Println(len(s), cap(s)) //5 8
}

我们看到,append 会根据切片的需要,在当前底层数组容量无法满足的情况下,动态分配新的数组,新数组长度会按一定规律扩展。在上面这段代码中,针对元素是 int 型的数组,新数组的容量是当前数组的 2 倍。

新数组建立后,append 会把旧数组中的数据拷贝到新数组中,之后新数组便成为了切片的底层数组,旧数组会被垃圾回收掉。

小陷阱

不过 append 操作的这种自动扩容行为,有些时候会给我们开发者带来一些困惑,比如基于一个已有数组建立的切片,一旦追加的数据操作触碰到切片的容量上限(实质上也是数组容量的上界),切片就会和原数组解除“绑定”,后续对切片的任何修改都不会反映到原数组中了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func main() {
u := [...]int{11, 12, 13, 14, 15}
fmt.Println("array:", u) // array: [11 12 13 14 15]
s := u[1:3]
fmt.Printf("slice(len=%d, cap=%d): %v\n", len(s), cap(s),s) // slice(len=2, cap=4): [12 13]
s = append(s, 24)
fmt.Println("after append 24, array:", u) // after append 24, array: [11 12 13 24 15]
fmt.Printf("after append 24, slice(len=%d, cap=%d): %v\n", len(s), cap(s), s) // after append 24, slice(len=3, cap=4): [12 13 24]
s = append(s, 25)
fmt.Println("after append 25, array:", u) // after append 25, array: [11 12 13 24 25]
fmt.Printf("after append 25, slice(len=%d, cap=%d): %v\n", len(s), cap(s), s) // after append 25, slice(len=4, cap=4): [12 13 24 25]
s = append(s, 26)
fmt.Println("after append 26, array:", u) // after append 26, array: [11 12 13 24 25]
fmt.Printf("after append 26, slice(len=%d, cap=%d): %v\n", len(s), cap(s), s) // after append 26, slice(len=5, cap=8): [12 13 24 25 26]

s[0] = 22
fmt.Println("after reassign 1st elem of slice, array:", u) // after reassign 1st elem of slice, array: [11 12 13 24 25]
fmt.Printf("after reassign 1st elem of slice, slice(len=%d, cap=%d): %v\n", len(s), cap(s), s) // after reassign 1st elem of slice, slice(len=5, cap=8): [22 13 24 25 26]
}

在 append 25 之后,切片的元素已经触碰到了底层数组 u 的边界了。
然后我们再append 26 之后,append 发现底层数组已经无法满足 append 的要求,于是新创建了一个底层数组(数组长度为 cap(s) 的 2 倍,即 8),并将 slice 的元素拷贝到新数组中了。

在这之后,我们即便再修改切片的第一个元素值,原数组 u 的元素也不会发生改变了,因为这个时候切片 s 与数组 u 已经解除了“绑定关系”,s 已经不再是数组 u 的“描述符”了。

------ 本文结束------
如果本篇文章对你有帮助,可以给作者加个鸡腿~(*^__^*),感谢鼓励与支持!