了解Go中的数组和切片

简介

在Go语言中,数组切片都是由有序元素序列组成的数据结构。这些数据集合在处理许多相关值时非常有用。它们可以帮助你将属于一起的数据放在一起,压缩你的代码,并一次性对多个值执行相同的方法和操作。

尽管Go中的数组和切片都是有序元素序列,但两者之间有显著的区别。Go中的数组是一种数据结构,它包含一个在创建时定义容量的有序元素序列。一旦数组分配了其大小,大小就无法再更改。另一方面,切片是数组的可变长度版本,为使用这些数据结构的开发者提供了更多灵活性。切片构成了其他语言中所认为的数组。

鉴于这些区别,有时你会选择其中一个。如果你是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`,索引分解如下:

“blue coral” “staghorn coral” “pillar coral” “elkhorn coral”
0 1 2 3

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

因为每个数组或切片中的元素都有一个对应的索引编号,我们可以像处理其他顺序数据类型一样访问和操作它们。

现在我们可以通过索引号来调用数组或切片中的一个离散元素:

fmt.Println(coral[1])
Output
staghorn 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])
Output
panic: runtime error: index out of range

在Go中,你不能使用负数索引,就像一些语言那样允许你向后索引;这样做会导致错误:

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

当我们使用一个正数索引来索引一个数组或切片时,就可以像这样使用+运算符连接字符串元素:

fmt.Println("Sammy loves " + coral[0])
Output
Sammy 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的长度,你会得到以下输出:

Output
4

这将给出数组coral的长度为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))

这将会得到如下输出:

Output
13

尽管这些示例数组中的项相对较少,但len()函数对于确定非常大型的数组中包含多少个元素非常有用。

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

使用append()向集合中追加元素

append()是Go内置的一个方法,用于向集合数据类型添加元素。但是,这个方法不能用于数组。如前所述,数组的主要区别在于其大小不能被修改。这意味着虽然你可以改变数组中元素的值,但你不能在数组定义之后使其变大或变小。

让我们考虑你的coral数组:

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

如果你想要把这个项目 "黑珊瑚" 添加到这个数组中,如果你尝试使用 append() 函数和数组通过输入:

coral = append(coral, "black coral")

你将收到一个错误,因为你的输出:

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

为了解决这个问题,让我们了解更多关于切片数据类型,如何定义一个切片,以及如何将数组转换为切片。

切片

在 Go 语言中,切片 是一种可变的数据类型,即可以改变顺序的元素序列。由于切片的尺寸是可变的,因此在使用它们时具有很大的灵活性;当处理将来可能需要扩展或缩小的数据集合时,使用切片将确保在尝试操作集合长度时不会遇到错误。在大多数情况下,这种可变性是值得的,尽管有时切片在内存重新分配方面可能比数组需要更多的时间。当你需要存储很多元素或者遍历元素并且希望能够方便地修改这些元素时,你可能会想使用切片数据类型。

定义一个切片

切片是通过声明数据类型前后的空方括号([])和花括号({})之间的元素来定义的。与需要方括号之间的整数来指定特定长度的数组不同,切片没有任何东西在方括号之间,表示其可变长度。

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

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,您首先需要学习如何从数组中切分部分。

切片数组

使用索引号来确定数组中值的范围,你可以调用数组的一个子段。这称为切片数组,并且可以通过创建一个用冒号分隔的索引范围来实现,形式为[第一个索引:第二个索引]。但是需要注意的是,当切片数组时,返回的是一个切片,而不是数组。

假设你想打印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[第一个索引:第二个索引]语法中省略其中一个数字。例如,如果你想打印数组的前三个项目——即"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]

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

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()函数所需的任何额外内存分配。

构建多维切片

您还可以定义包含其他切片作为元素的切片,每个括号内的列表都包含在大括号内。这样的集合称为多维切片。这些可以看作是描绘多维坐标;例如,一个由五个切片组成的集合,每个切片有六个元素,可以表示一个水平长度为五、垂直高度为六的两维网格。

让我们看看下面的多维切片:

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

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

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

在前面的代码中,我们首先标识了切片在索引为1的位置上的第一个元素,然后指示切片在索引为0的位置上的第一个元素。这将得到以下结果:

Output
Sammy 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中的数据结构,请参阅我们的文章《如何在Go中理解映射》,或者探索整个《如何在Go中编码》系列。

Source:
https://www.digitalocean.com/community/tutorials/understanding-arrays-and-slices-in-go