Go语言基本数据结构之数组与切片 | 青训营笔记

114 阅读5分钟

首先,参加本次后端青训营的大家,肯定都是冲着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))

运行的结果是:

image.png