Go 数组和切片简介

了解在 Go 中使用数组和切片存储数据的优缺点,以及为什么通常选择其中一个比另一个更好。
229 位读者喜欢这篇文章。
Testing certificate chains with a 34-line Go program

carrotmadman6。由 Opensource.com 修改。CC BY-SA 2.0

本文是 Mihalis Tsoukalos 撰写的 Go 系列文章的一部分

在本系列的第四篇文章中,我将解释 Go 数组和切片,如何使用它们,以及为什么您通常会选择其中一个而不是另一个。

数组

数组是编程语言中最流行的数据结构之一,原因有二:它们简单易懂,并且可以存储多种不同类型的数据。

您可以声明一个名为 anArray 的 Go 数组,用于存储四个整数,如下所示

anArray := [4]int{-1, 2, 0, -4}

数组的大小应在其类型之前声明,类型应在其元素之前定义。len() 函数可以帮助您找到任何数组的长度。上面数组的大小为 4。

如果您熟悉其他编程语言,您可能会尝试使用 for 循环访问数组的所有元素。但是,正如您将在下面看到的,Go 的 range 关键字允许您以更优雅的方式访问数组或切片的所有元素。

最后,这是定义二维数组的方法

twoD := [3][3]int{
    {1, 2, 3},
    {6, 7, 8},
    {10, 11, 12}}

arrays.go 源代码文件解释了 Go 数组的用法。arrays.go 中最重要的代码是

for i := 0; i < len(twoD); i++ {
        k := twoD[i]
        for j := 0; j < len(k); j++ {
                fmt.Print(k[j], " ")
        }
        fmt.Println()
}

for _, a := range twoD {
        for _, j := range a {
                fmt.Print(j, " ")
        }
        fmt.Println()
}

这展示了如何使用 for 循环和 range 关键字迭代数组的元素。arrays.go 的其余代码展示了如何将数组作为参数传递给函数。

以下是 arrays.go 的输出

$ go run arrays.go
Before change(): [-1 2 0 -4]
After change(): [-1 2 0 -4]
1 2 3
6 7 8
10 11 12
1 2 3
6 7 8
10 11 12

此输出表明,您在函数内部对数组所做的更改在函数退出后会丢失。

数组的缺点

Go 数组有很多缺点,会让您重新考虑在 Go 项目中使用它们。首先,您在定义数组后无法更改其大小,这意味着 Go 数组不是动态的。简单来说,如果您需要向没有剩余空间的数组添加元素,则需要创建一个更大的数组并将旧数组的所有元素复制到新数组中。其次,当您将数组作为参数传递给函数时,您实际上是传递了数组的副本,这意味着您在函数内部对数组所做的任何更改在函数退出后都会丢失。最后,将大型数组传递给函数可能会非常慢,主要是因为 Go 必须创建数组的副本。

解决所有这些问题的方法是使用 Go 切片。

切片

Go 切片类似于 Go 数组,但没有缺点。首先,您可以使用 append() 函数向现有切片添加元素。此外,Go 切片在内部使用数组实现,这意味着 Go 为每个切片使用一个底层数组。

切片具有容量属性和长度属性,这两个属性并不总是相同的。切片的长度与具有相同元素数量的数组的长度相同,可以使用 len() 函数找到。切片的容量是当前为切片分配的空间,可以使用 cap() 函数找到。

由于切片的大小是动态的,如果切片空间不足(这意味着数组的当前长度与其容量相同,而您正尝试向数组添加另一个元素),Go 会自动将其当前容量翻倍,以便为更多元素腾出空间,并将请求的元素添加到数组中。

此外,切片通过引用传递给函数,这意味着实际传递给函数的是切片变量的内存地址,并且您在函数内部对切片所做的任何修改在函数退出后都不会丢失。因此,将大型切片传递给函数比将具有相同元素数量的数组传递给同一函数要快得多。这是因为 Go 不必制作切片的副本——它只会传递切片变量的内存地址。

Go 切片在 slice.go 中进行了说明,其中包含以下代码

package main

import (
        "fmt"
)

func negative(x []int) {
        for i, k := range x {
                x[i] = -k
        }
}

func printSlice(x []int) {
        for _, number := range x {
                fmt.Printf("%d ", number)
        }
        fmt.Println()
}

func main() {
        s := []int{0, 14, 5, 0, 7, 19}
        printSlice(s)
        negative(s)
        printSlice(s)

        fmt.Printf("Before. Cap: %d, length: %d\n", cap(s), len(s))
        s = append(s, -100)
        fmt.Printf("After. Cap: %d, length: %d\n", cap(s), len(s))
        printSlice(s)

        anotherSlice := make([]int, 4)
        fmt.Printf("A new slice with 4 elements: ")
        printSlice(anotherSlice)
}

切片定义和数组定义之间最大的区别在于,您不需要指定切片的大小,切片的大小由您要放入其中的元素数量决定。此外,append() 函数允许您向现有切片添加元素——请注意,即使切片的容量允许您向该切片添加元素,除非您调用 append(),否则其长度不会被修改。printSlice() 函数是一个辅助函数,用于打印其切片参数的元素,而 negative() 函数处理其切片参数的所有元素。

slice.go 的输出是

$ go run slice.go
0 14 5 0 7 19
0 -14 -5 0 -7 -19
Before. Cap: 6, length: 6
After. Cap: 12, length: 7
0 -14 -5 0 -7 -19 -100
A new slice with 4 elements: 0 0 0 0

请注意,当您创建一个新切片并为给定数量的元素分配内存空间时,Go 会自动将所有元素初始化为其类型的零值,在本例中为 0。

使用切片引用数组

Go 允许您使用 [:] 表示法使用切片引用现有数组。在这种情况下,您对切片函数所做的任何更改都将传播到数组——这在 refArray.go 中进行了说明。请记住,[:] 表示法不会创建数组的副本,而只是对其的引用。

refArray.go 中最有趣的部分是

func main() {
        anArray := [5]int{-1, 2, -3, 4, -5}
        refAnArray := anArray[:]

        fmt.Println("Array:", anArray)
        printSlice(refAnArray)
        negative(refAnArray)
        fmt.Println("Array:", anArray)
}

refArray.go 的输出是

$ go run refArray.go
Array: [-1 2 -3 4 -5]
-1 2 -3 4 -5
Array: [1 -2 3 -4 5]

因此,由于对 anArray 数组的切片引用,anArray 数组的元素发生了更改。

总结

尽管 Go 同时支持数组和切片,但现在应该清楚的是,您很可能会使用切片,因为它们比 Go 数组更通用、更强大。只有少数情况下您需要使用数组而不是切片。最明显的一种情况是,当您绝对确定需要存储固定数量的元素时。

您可以在 GitHub 上找到 arrays.goslice.gorefArray.go 的 Go 代码。

如果您有任何问题或反馈,请在下方留言或在 Twitter 上联系我。

User profile image.
Mihalis Tsoukalos 是一位技术作家、UNIX 管理员和开发人员、DBA 和数学家。他是《Go 系统编程》和《精通 Go》的作者。您可以通过 http://www.mtsoukalos.eu/ 和 https://twitter.com/mactsouk 联系他。

评论已关闭。

Creative Commons License本作品根据 Creative Commons Attribution-Share Alike 4.0 International License 获得许可。
© . All rights reserved.