理解Go中的数组和切片

来自菜鸟教程
跳转至:导航、​搜索

介绍

在 Go 中,arraysslices 是由有序元素序列组成的 数据结构 。 当您想要处理许多相关值时,这些数据集合非常适合使用。 它们使您能够将属于一起的数据保存在一起,压缩您的代码,并一次对多个值执行相同的方法和操作。

尽管 Go 中的数组和切片都是元素的有序序列,但两者之间存在显着差异。 Go 中的 array数据结构 ,由有序的元素序列组成,其容量在创建时定义。 一旦数组分配了它的大小,就不能再改变大小。 另一方面,slice 是数组的可变长度版本,为使用这些数据结构的开发人员提供了更大的灵活性。 切片构成了您在其他语言中所认为的数组。

鉴于这些差异,在某些特定情况下您会使用其中一种。 如果您是 Go 新手,确定何时使用它们可能会令人困惑:尽管切片的多功能性使它们在大多数情况下成为更合适的选择,但在某些特定情况下,数组可以优化程序的性能。

本文将详细介绍数组和切片,这些阵列和切片将为您提供必要的信息,以在这些数据类型之间进行选择时做出适当的选择。 此外,您将了解声明和使用数组和切片的最常用方法。 本教程将首先提供数组的描述以及如何操作它们,然后解释切片及其不同之处。

数组

数组是具有一定数量元素的集合数据结构。 因为数组的大小是静态的,所以数据结构只需要分配一次内存,而变长数据结构则必须动态分配内存,以便将来可以变大或变小。 尽管数组的固定长度会使它们的使用有些僵硬,但一次性内存分配可以提高程序的速度和性能。 因此,在数据结构永远不需要可变数量的元素的情况下,开发人员通常在优化程序时使用数组。

定义一个数组

数组是通过在括号 [ ] 中声明数组的大小来定义的,然后是元素的数据类型。 Go 中的数组必须具有相同的 数据类型 。 在数据类型之后,您可以在大括号 { } 中声明数组元素的各个值。

以下是声明数组的一般模式:

[capacity]data_type{element_values}

注意: 重要的是要记住,新数组的每个声明都会创建一个不同的类型。 因此,尽管 [2]int[3]int 都具有整数元素,但它们的不同长度使得它们的数据类型不兼容。


如果不声明数组元素的值,则默认为零值,这意味着数组的元素将为空。 对于整数,这由 0 表示,对于字符串,这由一个空字符串表示。

例如,以下数组 numbers 包含三个尚无值的整数元素:

var numbers [3]int

如果您打印了 numbers,您将收到以下输出:

Output[0 0 0]

如果您想在创建数组时分配元素的值,请将值放在大括号中。 具有设定值的字符串数组如下所示:

[4]string{"blue coral", "staghorn coral", "pillar coral", "elkhorn coral"}

您可以将数组存储在变量中并打印出来:

coral := [4]string{"blue coral", "staghorn coral", "pillar coral", "elkhorn coral"}
fmt.Println(coral)

运行带有前面几行的程序会给你以下输出:

Output[blue coral staghorn coral pillar coral elkhorn coral]

请注意,打印时数组中的元素之间没有界限,因此很难分辨一个元素的结束位置和另一个元素的开始位置。 因此,有时使用 fmt.Printf 函数会有所帮助,它可以在将字符串打印到屏幕之前对其进行格式化。 使用此命令提供 %q 动词以指示函数在值周围加上引号:

fmt.Printf("%q\n", coral)

这将导致以下结果:

Output["blue coral" "staghorn coral" "pillar coral" "elkhorn coral"]

现在每个项目都被引用。 \n 动词指示格式化程序在末尾添加一个换行符。

大致了解如何声明数组及其组成内容后,您现在可以继续学习如何使用索引号指定数组中的元素。

索引数组(和切片)

数组(以及切片)中的每个元素都可以通过索引单独调用。 每个元素对应一个索引号,即从索引号0开始向上计数的int值。

我们将在以下示例中使用数组,但您也可以使用切片,因为它们的索引方式相同。

对于数组 coral,索引分解如下所示:

“蓝珊瑚” “鹿角珊瑚” “柱状珊瑚” “麋鹿角珊瑚”
0 1 2 3

第一个元素,字符串 "blue coral",从索引 0 开始,切片以元素 "elkhorn coral" 的索引 3 结束。

因为切片或数组中的每个元素都有相应的索引号,所以我们能够以与其他顺序数据类型相同的方式访问和操作它们。

现在我们可以通过引用它的索引号来调用切片的离散元素:

fmt.Println(coral[1])
Outputstaghorn coral

该切片的索引号范围为 0-3,如上表所示。 因此,要单独调用任何元素,我们将像这样引用索引号:

coral[0] = "blue coral"
coral[1] = "staghorn coral"
coral[2] = "pillar coral"
coral[3] = "elkhorn coral"

如果我们用任何大于 3 的索引号调用数组 coral,它将超出范围,因为它无效:

fmt.Println(coral[18])
Outputpanic: runtime error: index out of range

索引数组或切片时,必须始终使用正数。 与某些允许您使用负数向后索引的语言不同,在 Go 中这样做会导致错误:

fmt.Println(coral[-1])
Outputinvalid array index -1 (index must be non-negative)

我们可以使用 + 运算符将数组或切片中的字符串元素与其他字符串连接起来:

fmt.Println("Sammy loves " + coral[0])
OutputSammy loves blue coral

我们能够将索引号 0 处的字符串元素与字符串 "Sammy loves " 连接起来。

通过与数组或切片中的元素对应的索引号,我们能够离散地访问每个元素并使用这些元素。 为了证明这一点,我们接下来看看如何修改某个索引处的元素。

修改元素

我们可以使用索引来更改数组或切片中的元素,方法是将索引编号的元素设置为不同的值。 这使我们能够更好地控制切片和数组中的数据,并允许我们以编程方式操作单个元素。

如果我们想将数组 coral 的索引 1 处的元素的字符串值从 "staghorn coral" 更改为 "foliose coral",我们可以这样做:

coral[1] = "foliose coral"

现在当我们打印 coral 时,数组会有所不同:

fmt.Printf("%q\n", coral)
Output["blue coral" "foliose coral" "pillar coral" "elkhorn coral"]

现在您已经知道如何操作数组或切片的各个元素,让我们看看几个函数,它们可以在处理集合数据类型时为您提供更大的灵活性。

使用 len() 计数元素

在 Go 中,len() 是一个内置函数,可帮助您处理数组和切片。 与字符串一样,您可以通过使用 len() 并将数组或切片作为参数传入来计算数组或切片的长度。

例如,要查找 coral 数组中有多少元素,您可以使用:

len(coral)

如果您打印出数组 coral 的长度,您将收到以下输出:

Output4

这给出了 int 数据类型中数组 4 的长度,这是正确的,因为数组 coral 有四项:

coral := [4]string{"blue coral", "foliose coral", "pillar coral", "elkhorn coral"}

如果你创建一个包含更多元素的整数数组,你也可以使用 len() 函数:

numbers := [13]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}
fmt.Println(len(numbers))

这将导致以下输出:

Output13

尽管这些示例数组的项相对较少,但 len() 函数在确定非常大的数组中有多少元素时特别有用。

接下来,我们将介绍如何将元素添加到集合数据类型,并演示由于数组的固定长度,附加这些静态数据类型将如何导致错误。

使用 append() 附加元素

append() 是 Go 中的内置方法,可将元素添加到集合数据类型。 但是,当与数组一起使用时,此方法将不起作用。 如前所述,数组与切片的主要不同之处在于数组的大小无法修改。 这意味着虽然您可以更改数组中元素的值,但您不能在定义数组后使数组变大或变小。

让我们考虑您的 coral 数组:

coral := [4]string{"blue coral", "foliose coral", "pillar coral", "elkhorn coral"}

假设您想将项目 "black coral" 添加到此数组中。 如果您尝试通过键入以下命令对数组使用 append() 函数:

coral = append(coral, "black coral")

您将收到一个错误作为输出:

Outputfirst argument to append must be slice; have [4]string

为了解决这个问题,让我们进一步了解切片数据类型、如何定义切片以及如何从数组转换为切片。

切片

slice 是 Go 中的一种数据类型,它是 可变 或可变的有序元素序列。 由于切片的大小是可变的,因此使用它们时具有更大的灵活性; 在处理将来可能需要扩展或收缩的数据集合时,使用切片将确保您的代码在尝试操纵集合的长度时不会出错。 在大多数情况下,与数组相比,这种可变性值得切片有时需要重新分配内存。 当您需要存储大量元素或迭代元素并且希望能够轻松修改这些元素时,您可能希望使用切片数据类型。

定义切片

切片是通过在一组空方括号 ([]) 和大括号之间的元素列表 ({}) 前声明数据类型来定义的。 您会注意到,与需要在括号之间使用 int 来声明特定长度的数组相反,切片在括号之间没有任何内容,表示其可变长度。

让我们创建一个包含字符串数据类型元素的切片:

seaCreatures := []string{"shark", "cuttlefish", "squid", "mantis shrimp", "anemone"}

当我们打印出切片时,我们可以看到切片中的元素:

fmt.Printf("%q\n", seaCreatures)

这将导致以下结果:

Output["shark" "cuttlefish" "squid" "mantis shrimp" "anemone"]

如果你想创建一个特定长度的切片而不填充集合的元素,你可以使用内置的 make() 函数:

oceans := make([]string, 3)

如果你打印这个切片,你会得到:

Output["" "" ""]

如果你想为某个容量预先分配内存,你可以将第三个参数传递给 make()

oceans := make([]string, 3, 5)

这将生成一个长度为 3 且预分配容量为 5 元素的零切片。

您现在知道如何声明切片了。 但是,这还没有解决我们之前使用 coral 数组时遇到的错误。 要将 append() 函数与 coral 一起使用,您首先必须学习如何切出数组的各个部分。

将数组切片成切片

通过使用索引号来确定起点和终点,您可以调用数组中值的子部分。 这称为 slicing 数组,您可以通过以 [first_index:second_index] 的形式创建由冒号分隔的索引号范围来实现此目的。 然而,重要的是要注意,当对数组进行切片时,结果是切片,而不是数组。

假设您只想打印 coral 数组的中间项,没有第一个和最后一个元素。 您可以通过创建一个从索引 1 开始并在索引 3 之前结束的切片来执行此操作:

fmt.Println(coral[1:3])

使用这一行运行程序将产生以下结果:

Output[foliose coral pillar coral]

创建切片时,如在 [1:3] 中,第一个数字是切片开始的位置(包括),第二个数字是第一个数字与您要检索的元素总数的总和:

array[starting_index : (starting_index + length_of_slice)]

在本例中,您调用第二个元素(或索引 1)作为起点,总共调用了两个元素。 这是计算的样子:

array[1 : (1 + 2)]

这就是您获得此符号的方式:

coral[1:3]

如果要将数组的开头或结尾设置为切片的起点或终点,则可以省略 array[first_index:second_index] 语法中的数字之一。 例如,如果你想打印数组 coral 的前三个项目——这将是 "blue coral""foliose coral""pillar coral"——你可以这样做所以通过输入:

fmt.Println(coral[:3])

这将打印:

Output[blue coral foliose coral pillar coral]

这打印了数组的开头,在索引 3 之前停止。

要在数组末尾包含所有项目,您可以反转语法:

fmt.Println(coral[1:])

这将给出以下切片:

Output[foliose coral pillar coral elkhorn coral]

本节讨论了通过切分小节来调用数组的各个部分。 接下来,您将学习如何使用切片将整个数组转换为切片。

从数组转换为切片

如果您创建一个数组并决定需要它具有可变长度,则可以将其转换为切片。 要将数组转换为切片,请使用您在本教程的 将数组切片为切片 步骤中学习的切片过程,但这次通过省略将确定端点的两个索引号来选择整个切片:

coral[:]

请记住,您不能将变量 coral 转换为切片本身,因为一旦在 Go 中定义了变量,就无法更改其类型。 要解决此问题,您可以将数组的全部内容作为切片复制到一个新变量中:

coralSlice := coral[:]

如果您打印了 coralSlice,您将收到以下输出:

Output[blue coral foliose coral pillar coral elkhorn coral]

现在,尝试像在数组部分中一样添加 black coral 元素,将 append() 与新转换的切片一起使用:

coralSlice = append(coralSlice, "black coral")
fmt.Printf("%q\n", coralSlice)

这将输出带有添加元素的切片:

Output["blue coral" "foliose coral" "pillar coral" "elkhorn coral" "black coral"]

我们还可以在单个 append() 语句中添加多个元素:

coralSlice = append(coralSlice, "antipathes", "leptopsammia")
Output["blue coral" "foliose coral" "pillar coral" "elkhorn coral" "black coral" "antipathes" "leptopsammia"]

要将两个切片组合在一起,可以使用 append(),但必须使用 ... 扩展语法扩展第二个参数以追加:

moreCoral := []string{"massive coral", "soft coral"}
coralSlice = append(coralSlice, moreCoral...)
Output["blue coral" "foliose coral" "pillar coral" "elkhorn coral" "black coral" "antipathes" "leptopsammia" "massive coral" "soft coral"]

现在您已经了解了如何将元素附加到切片中,我们将看看如何删除一个元素。

从切片中移除元素

与其他语言不同,Go 不提供任何内置函数来从切片中删除元素。 需要通过切片将项目从切片中删除。

要删除一个元素,您必须切出该元素之前的项目,切出该元素之后的项目,然后将这两个新切片附加在一起,而不包含您要删除的元素。

如果 i 是要删除的元素的索引,则此过程的格式如下所示:

slice = append(slice[:i], slice[i+1:]...)

coralSlice 中删除项目 "elkhorn coral"。 该项目位于3的索引位置。

coralSlice := []string{"blue coral", "foliose coral", "pillar coral", "elkhorn coral", "black coral", "antipathes", "leptopsammia", "massive coral", "soft coral"}

coralSlice = append(coralSlice[:3], coralSlice[4:]...)

fmt.Printf("%q\n", coralSlice)
Output["blue coral" "foliose coral" "pillar coral" "black coral" "antipathes" "leptopsammia" "massive coral" "soft coral"]

现在索引位置 3 处的元素,字符串 "elkhorn coral",不再在我们的切片 coralSlice 中。

我们也可以用同样的方法删除一个范围。 假设我们不仅要删除项目 "elkhorn coral",还要删除 "black coral""antipathes"。 我们可以在表达式中使用范围来完成此操作:

coralSlice := []string{"blue coral", "foliose coral", "pillar coral", "elkhorn coral", "black coral", "antipathes", "leptopsammia", "massive coral", "soft coral"}

coralSlice = append(coralSlice[:3], coralSlice[6:]...)

fmt.Printf("%q\n", coralSlice)

此代码将从切片中取出索引 345

Output["blue coral" "foliose coral" "pillar coral" "leptopsammia" "massive coral" "soft coral"]

现在您已经知道如何在切片中添加和删除元素,让我们看看如何测量切片在任何给定时间可以容纳的数据量。

cap() 测量切片的容量

由于切片具有可变长度,因此 len() 方法不是确定此数据类型大小的最佳选择。 相反,您可以使用 cap() 函数来学习切片的容量。 这将显示一个切片可以容纳多少个元素,这取决于已经为切片分配了多少内存。

注意:因为数组的长度和容量总是相同的,所以cap()函数对数组不起作用。


cap() 的一个常见用途是创建具有预设数量元素的切片,然后以编程方式填充这些元素。 这避免了通过使用 append() 添加超出当前分配容量的元素可能发生的潜在不必要的分配。

让我们假设我们想要制作一个数字列表,从 03。 我们可以在循环中使用 append() 来执行此操作,或者我们可以先预分配切片并使用 cap() 循环以填充值。

首先,我们可以看看使用append()

numbers := []int{}
for i := 0; i < 4; i++ {
    numbers = append(numbers, i)
}
fmt.Println(numbers)
Output[0 1 2 3]

在这个例子中,我们创建了一个切片,然后创建了一个循环四次的 for 循环。 每次迭代都将循环变量 i 的当前值附加到 numbers 切片的索引中。 但是,这可能会导致不必要的内存分配,从而减慢您的程序。 添加到空切片时,每次调用 append 时,程序都会检查切片的容量。 如果添加的元素使切片超出此容量,则程序将分配额外的内存来解决它。 这会在您的程序中产生额外的开销,并可能导致执行速度变慢。

现在让我们通过预先分配一定的长度/容量来填充切片而不使用 append()

numbers := make([]int, 4)
for i := 0; i < cap(numbers); i++ {
    numbers[i] = i
}

fmt.Println(numbers)
Output[0 1 2 3]

在这个例子中,我们使用 make() 创建一个切片并让它预分配 4 元素。 然后,我们在循环中使用 cap() 函数遍历每个归零的元素,填充每个元素,直到达到预先分配的容量。 在每个循环中,我们将循环变量 i 的当前值放入 numbers 切片的索引中。

虽然 append()cap() 策略在功能上都是等效的,但 cap() 示例避免了使用 append() 函数所需的任何额外内存分配。

构造多维切片

您还可以将由其他切片组成的切片定义为元素,每个带括号的列表都包含在父切片的较大括号内。 像这样的切片集合称为多维切片。 这些可以被认为是描述多维坐标; 例如,五个切片的集合,每个切片有六个元素长,可以表示一个二维网格,其中水平长度为 5,垂直高度为 6。

让我们检查以下多维切片:

seaNames := [][]string{{"shark", "octopus", "squid", "mantis shrimp"}, {"Sammy", "Jesse", "Drew", "Jamie"}}

要访问此切片中的元素,我们将不得不使用多个索引,一个用于构造的每个维度:

fmt.Println(seaNames[1][0])
fmt.Println(seaNames[0][0])

在上面的代码中,我们首先识别索引 1 的切片的索引 0 处的元素,然后我们指出索引 0。 这将产生以下结果:

OutputSammy
shark

以下是其余单个元素的索引值:

seaNames[0][0] = "shark"
seaNames[0][1] = "octopus"
seaNames[0][2] = "squid"
seaNames[0][3] = "mantis shrimp"

seaNames[1][0] = "Sammy"
seaNames[1][1] = "Jesse"
seaNames[1][2] = "Drew"
seaNames[1][3] = "Jamie"

使用多维切片时,请务必记住,您需要引用多个索引号才能访问相关嵌套切片中的特定元素。

结论

在本教程中,您学习了在 Go 中使用数组和切片的基础。 您经历了多个练习来演示数组的长度是如何固定的,而切片的长度是可变的,并发现了这种差异如何影响这些数据结构的情景使用。

要继续研究 Go 中的数据结构,请查看我们关于 Understanding Maps in Go 的文章,或探索整个 How To Code in Go 系列。