什么是数组
数组有哪些基本特性
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 | fmt.Println(len(nums)) // 6 |
Go是如何实现切片类型的
Go 切片在运行时其实是一个三元组结构,它在 Go 运行时中的表示如下:
1 | type slice struct { |
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 | arr := [10]int{1,2,3,4,5,6,7,8,9,10} |
通过类比,发现切片好比打开了一个访问与修改数组的“窗口”,通过这个窗口,我们可以直接操作底层数组中的部分元素。
在 Go 语言中,数组更多是“退居幕后”,承担的是底层存储空间的角色。切片就是数组的“描述符”,也正是因为这一特性,切片才能在函数参数传递时避免较大性能开销。
另外,针对一个已存在的数组,我们还可以建立多个操作数组的切片,这些切片共享同一底层数组,切片对底层数组的操作也同样会反映到其他切片中。
基于切片创建切片
切片的动态扩容
“动态扩容”指的就是,当我们通过 append 操作向切片追加数据的时候,如果这时切片的 len 值和 cap 值是相等的,也就是说切片底层数组已经没有空闲空间再来存储追加的值了,Go 运行时就会对这个切片做扩容操作,来保证切片始终能存储下追加的新值。
例如:
1 | func main() { |
我们看到,append 会根据切片的需要,在当前底层数组容量无法满足的情况下,动态分配新的数组,新数组长度会按一定规律扩展。在上面这段代码中,针对元素是 int 型的数组,新数组的容量是当前数组的 2 倍。
新数组建立后,append 会把旧数组中的数据拷贝到新数组中,之后新数组便成为了切片的底层数组,旧数组会被垃圾回收掉。
小陷阱
不过 append 操作的这种自动扩容行为,有些时候会给我们开发者带来一些困惑,比如基于一个已有数组建立的切片,一旦追加的数据操作触碰到切片的容量上限(实质上也是数组容量的上界),切片就会和原数组解除“绑定”,后续对切片的任何修改都不会反映到原数组中了。
1 | func main() { |
在 append 25 之后,切片的元素已经触碰到了底层数组 u 的边界了。
然后我们再append 26 之后,append 发现底层数组已经无法满足 append 的要求,于是新创建了一个底层数组(数组长度为 cap(s) 的 2 倍,即 8),并将 slice 的元素拷贝到新数组中了。
在这之后,我们即便再修改切片的第一个元素值,原数组 u 的元素也不会发生改变了,因为这个时候切片 s 与数组 u 已经解除了“绑定关系”,s 已经不再是数组 u 的“描述符”了。