首先,参加本次后端青训营的大家,肯定都是冲着Go语言来的了。其实最近看面经中,发现基本的数据结构数组和切片其实是面试中从常考题,Go语言中的切片也确实为这门语言带来了很多不一样的气息,也为开发带来了更多的可能与方便。
数组
先来说一下数组吧,这是几乎所有语言中都会有的一种数据结构,用来存储顺序的一组数据。数组有哪些特性呢?
Go 语言的数组是一个长度固定的、由同构类型元素组成的连续序列。通过这个定义,我们可以识别出 Go 的数组类型包含两个重要属性:元素的类型和数组长度(元素的个数)。这两个属性也直接构成了 Go 语言中数组类型变量的声明:
var arr [N]T
声明了一个数组变量 arr,它的类型为[N]T,其中元素的类型为 T,数组的长度为 N。这里,我们要注意,数组元素的类型可以为任意的 Go 原生类型或自定义类型,而且数组的长度必须在声明数组变量时提供,Go 编译器需要在编译阶段就知道数组类型的长度,所以,我们只能用整型数字面值或常量表达式作为 N 值。
这也是数组的一个特点,长度一旦声明就不能改变了。
这里也有一个点值得注意下,基于长度不可变,也就说明,如果两个数组类型的元素类型 T 与数组长度 N 都是一样的,那么这两个数组类型是等价的,如果有一个属性不同,它们就是两个不同的数组类型。
从定长数组到变长切片
数组类型变量是一个整体,这就意味着一个数组变量表示的是整个数组。这点与 C 语言完全不同,在 C 语言中,数组变量可视为指向数组第一个元素的指针。这样一来,无论是参与迭代,还是作为实际参数传给一个函数 / 方法,Go 传递数组的方式都是纯粹的值拷贝,这会带来较大的内存拷贝开销。
Go 语言为我们提供了一种更为灵活、更为地道的方式 ,切片,来解决这个问题。
这里先初始化一个切片变量:
var s1 []int{1, 2, 3, 4, 5, 6}
切片声明仅仅是少了一个“长度”属性,这也是切片灵活的一大原因。
虽然不需要像数组那样在声明时指定长度,但切片也有自己的长度,只不过这个长度不是固定的,而是随着切片中元素个数的变化而变化的。我们可以通过 len 函数获得切片类型变量的长度,比如上面那个切片变量的长度就是 6:
fmt.Println(len(s1)) // 6
通过 Go 内置函数 append,我们可以动态地向切片中添加元素。当然,添加后切片的长度也就随之发生了变化,如下面代码所示:
s1 = append(s1, 7)
fmt.Println(len(s1)) // 7
运行时中的切片:
type slice struct {
array unsafe.Pointer
len int
cap int
}
我们可以看到,每个切片包含三个字段:
- array: 是指向底层数组的指针;
- len: 是切片的长度,即切片中当前元素的个数;
- cap: 是底层数组的长度,也是切片的最大容量,cap 值永远大于等于 len 值。
创建切片的其他方式
make
通过 make 函数来创建切片,并指定底层数组的长度。
s2 := make([]int, 6, 10) // 其中10为cap值,即底层数组长度,6为切片的初始长度
也可以这样
sl := make([]byte, 6) // cap = len = 6
数组切片化
采用 array[low : high : max]语法基于一个已存在的数组创建切片。这种方式被称为数组的切片化
arr := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
sl := arr[3:7:9]
切片好比打开了一个访问与修改数组的“窗口”,通过这个窗口,我们可以直接操作底层数组中的部分元素。这有些类似于我们操作文件之前打开的“文件描述符”(Windows 上称为句柄),通过文件描述符我们可以对底层的真实文件进行相关操作。可以说,切片之于数组就像是文件描述符之于文件。
切片的动态扩容
“动态扩容”指的就是,当我们通过 append 操作向切片追加数据的时候,如果这时切片的 len 值和 cap 值是相等的,也就是说切片底层数组已经没有空闲空间再来存储追加的值了,Go 运行时就会对这个切片做扩容操作,来保证切片始终能存储下追加的新值。
append 会根据切片的需要,在当前底层数组容量无法满足的情况下,动态分配新的数组,新数组长度会按一定规律扩展。在上面这段代码中,针对元素是 int 型的数组,新数组的容量是当前数组的 2 倍。新数组建立后,append 会把旧数组中的数据拷贝到新数组中,之后新数组便成为了切片的底层数组,旧数组会被垃圾回收掉。
例子:
var s []int
s = append(s, 11)
fmt.Println("len(s) = ", len(s), "; cap(S) = ", cap(s))
s = append(s, 11)
fmt.Println("len(s) = ", len(s), "; cap(S) = ", cap(s))
s = append(s, 11)
fmt.Println("len(s) = ", len(s), "; cap(S) = ", cap(s))
s = append(s, 11)
fmt.Println("len(s) = ", len(s), "; cap(S) = ", cap(s))
s = append(s, 11)
fmt.Println("len(s) = ", len(s), "; cap(S) = ", cap(s))
运行的结果是: