[译] Golang 中的数组和切片指南(及其差异)

650

首先,很容易看到数组和切片好像是同一个东西:表示列表的数据结构。然而,它们实际上彼此完全不同。

在这篇文章中,我们将探讨他们在 Go 中的差异和实现。

数组

数组是固定的数据列表。这里的重点是固定的,因为一旦设置了数组的长度,它就无法更改。

我们举一个声明了四个整数的数组的例子:

arr := [4]int{3, 2, 5, 4}

长度和类型

我们在上面的例子中定义的 arr 变量的类型是 [4] int,它是一个大小为 4 的数组。这里需要注意的是,4 包含在类型定义中。

这意味着两个不同长度的数组实际上是两个不同的类型。所以不能将不同长度的数组视为一种类型,也不能将其中一个的值分配给另一个:

longerArr := [5]int{5, 7, 1, 2, 0}

longerArr = arr
// 会抛出编译错误

longerArr == arr
// 会抛出编译错误

我发现考虑数组的一个好方法就是结构体。如果我们可以构造数组等价的结构体,它可能看起来像这样:

// 长度为 4 的数组的等价结构体
type int4 struct {
  e0 int
  e1 int
  e2 int
  e3 int
}

// 长度为 5 的数组的等价结构体
type int5 struct {
  e0 int
  e1 int
  e2 int
  e3 int
  e5 int
}

arr := int4{3, 2, 5, 4}
longerArr := int5{5, 7, 1, 2, 0}

不建议执行此操作,但这是一个很好的方法来了解为何不同长度的数组是完全不同的类型。

内存表示

数组存储为指定类型的 n 块的序列:

初始化数组类型的变量后,将立即分配此内存。

引用传递

在 Go 中,没有引用传递。一切都是通过值传递的。如果将数组的值分配给另一个变量,则会复制整个值。

如果只想将“引用”传递给数组,可以使用指针:

在内存分配和函数中,数组实际上是一种非常简单的数据类型,其工作方式与结构体相同。

切片

我们可以将切片视为基于数组的高级实现。

在 Go 中实现了切片,以涵盖开发人员在处理列表时面临的一些非常常见的需求,例如需要动态修改大小。

声明切片几乎与声明数组相同,除了需要必须省略长度的说明符:

slice := []int{4, 5, 3}

仅仅看代码的话,切片和数组看起来非常相似,但实际上在实现和使用方面存在显著差异。

内存表示

切片的分配方式与数组不同,实际上是修改过的指针。每个切片包含三条信息:

  1. 指向数据序列的指针
  2. 长度:表示当前包含的元素总数。
  3. 容量:即配置的内存位置总数。

然后,可以为彼此的值分配不同长度的切片。它们的类型相同,指针,长度和容量都在变化:

slice1 := []int{6, 1, 2}
slice2 := []int{9, 3}

// 可以将任何长度的切片分配给其他切片

与数组不同,切片在初始化期间不分配数据块的内存。实际上,切片用 nil 值初始化。

引用传递

将切片分配给另一个变量时,仍然按值传递。这里的值仅指代指针,长度和容量,而不是元素本身占用的内存。(译者注:这里做了一个实验可以更清晰地了解这个过程)

增加新元素

要向切片添加元素,通常使用 append 函数。

nums := []int{8, 0}
nums = append(nums, 8)

在内部,这会将指定的值分配给新元素,并返回一个新的切片。这个新切片的长度增加了 1。(译者注:关于切片的扩容分析可以参考煎鱼stefno 的博客)

这就是为什么经常建议创建一个预先指定长度和容量的切片(特别是如果你很清楚它的大小可能是多少):

arr := make([]int, 0, 5)
// 这将创建一个长度为 0 且容量为 5 的切片

数组和切片的使用场景

数组和切片是完全不同的,因此,它们的用例也是完全不同的。

我们来看一下开源项目和 Go 标准库中的一些例子,看看它们怎么使用的。

例子 1:UUID

UUID 是 128 位数据,通常用来唯一标记对象或实体。通常以短划线分隔的十六进制值表示:

e39bdaf4-710d-42ea-a29b-58c368b0c53c

Google 的 UUID 库 中,UUID 表示为 16 字节的数组:

type UUID [16]byte

这是有意义的,因为我们知道 UUID 是由 128 位(16 字节)组成的。我们不会在 UUID 中添加或删除任何字节,因此使用数组来表示会更好。

例子 2:整数排序

在下一个示例中,我们将查看排序标准库中的 sort.Ints 函数:

s := []int{5, 2, 6, 3, 1, 4} // unsorted
sort.Ints(s)
fmt.Println(s)
// [1 2 3 4 5 6]

sort.Ints 函数接受一个整数列表并将它们排序。这里选切片有两个原因:

  1. 未指定整数的数量(可以有任意数量的整数进行排序)。
  2. 这些数字需要原地排序。使用数组会将整个整数列表作为值传递,因此该函数只会对它的副本进行排序,而不是对传递给它的值。(译者注:因为不同长度的数组是不同的类型,所以 sort.Ints 规定了入参类型为切片,这也是一个比较重要的原因)

结论

现在我们已经介绍了数组和切片之间的关键差异及其用例,这里有一些提示可以决定哪种结构更合适:

  1. 如果实体由一组固定长度的非空项组成:使用数组。
  2. 需要对列表进行添加或删除元素时,请使用切片。
  3. 如果列表可以包含任意数量的元素,请使用切片。
  4. 你会以某种方式修改列表吗?如果是,则使用切片

我们可以看到,切片涵盖了在 Go 中创建应用程序的大多数场景。尽管如此,数组确实有它们的位置,并且在需要它们时非常有用。

你们有更好的例子吗?如果有任何关于你更喜欢切片胜于数组(反之亦然)的例子?请在下面的评论中告诉我们👇