[奔跑的 Go] 教程九、深入学习 Go 语言切片(Slice)

Golang

切片和数组相似,但是它们在长度上有所不同。

什么是切片

切片和数组相似,它们都是 保存相同数据类型元素的容器 但是切片 元素个数可变

切片是由基础数据类型组成,因此它是一种 复合数据类型  (查阅基础数据类型课程)。

除了不需要明确指定元素个数,定义一个切片的与定义一个数组数组的语法十分相似。 因此,s 就是一个切片

var s []int

上面的代码将会创建一个 int 类型的切片,意味着在这个切片里可以保存 int 类型的元素。有一个问题是:切片的零值是什么? 正如我们在数组中看到的那样,数组的零值是对应数据类型的零值。就像一个长度为 n 类型为 int的数组,将会有 n个 0 作为其默认值,因为 int 类型对应的零值是 0 。但是在切片 的情况下,像上面定义的切片的零值是nil 。下面的这段代码将会返回 true

https://play.golang.org/p/JI6ikCK2f9x

如果你问为什么是 nil。那是因为 切片只是对一个数组的引用

是不是*nil* ,*切片*的类型是 *[]Type*。上面的例子中, 切片 *s* 的类型是 *[]int*.

☛ slice 是对数组的引用

这可能听起来很奇怪,但是切片中不存储任何数据,数据是存储在数组中的。你可能会问这怎么可能,数组的长度不是固定的吗?

slice 需要扩容时,就会再创建一个合适长度的新数组,以存储更多数据。

当我们用 var s [] int 创建一个slice 时,它还没有引用任何数组,所以,它的值是 nil。下面来看一下如何引用数组。

创建一个数组,并将该数组中的一些元素复制到切片中。

https://play.golang.org/p/1naC_0qQz_E

在上面的程序中,我们定义了一个类型为 int 的切片 s,但是这个切片没有引用任何数组。因此,它的值是 nil, 并且第一个 Println 语句将输出 true

之后,我们创建了一个类型为 int 的数组 a,并为变量 s 赋值 a [2:4] 从而创建一个新的切片。 a [2:4] 的意思是,根据数组 a 生成一个切片,切片的值是取数组 a 中索引 2 到索引 3 的元素。我稍后会解释操作符 [:] 的用法。

现在,由于 s 引用了数组 a,所以它的值不是 nils 的值应该是 [3,4]

既然我们说 slice 是对数组的引用,那么,我们可以尝试修改数组的值,看是否会对 slice 产生影响。

在上面的程序中,对数组 a第三个第四个 元素的值(索引值分别为 23 )做一下修改,再来看下检查切片 s 的值。

https://play.golang.org/p/9xi8b8TTqHY

从上面的结果中,我们可以得出结论,slice 确实只是对数组的引用,并且对数组的任何更改,都会影响切片的值。

☛  slice 的长度和容量

正如我们在 数组 课程中了解到的,使用 len 函数可以查看数据的长度。这个函数对于切片同样适用。

https://play.golang.org/p/tKJaxdY7dYp

上面这段代码的输出为 “Length of s = 2”,这是正确的,因为 它只引用了数组 ` a ` 中的2个元素

切片的容量就是他能够容纳元素的数量,go提供内置函数cap来获取这个容量大小


https://play.golang.org/p/eAbelmHUkZK
上面的程序返回7,这就是切片的容量。
因为slice引用一个数组,所以它可以一直引用数组直到结束。由于从上面例子中的索引从2开始,数组中有7个元素,因此数组的容量为7。

这是否意味着我们可以在超出其自身容量的情况下进行切片?是可以的。我们可以使用append函数来实现。

☛ 切片 是一个 结构体

我们在接下来的课程中会学习 结构体 , 但结构体是由一些不同的类型的字段组成的一种 类型 ,这个结构体类型是依赖于这些组成变量的。

切片 结构体如下所示

type slice struct {
    zerothElement *type
    len int
    cap int
}

切片 结构体由指向切片引用数组的首元素的 zerothElement 指针组成的。len 和 cap 分别表示切片的长度和容量。 type 是(引用)数组中内部元素的类型。

因此,当定义新切片时,zerothElement 指针被设置为其零值,即「nil」。 但是当切片引用指向一个数组时,该指针的值将不是「nil」。

我们会在接下来的教程里学到更多关于 指针 的知识。但是下面的例子将显示 a [2]s [0]内存地址是相同的,这意味着他们指向内存中同一个元素。

https://play.golang.org/p/0jUjmjhTCos

0xc420018100 是用 16 进制表示内存里地址的数字。在不同的机器上得到的值可能不一样。

如果切片中元素的值发生改变,数组将会怎样呢? 这是一个非常好的问题。众所周知,切片本身不存储任何数据,而只是对数组的引用。因此,如果切片中元素的值发生改变,那么数组中元素的值也会相应地发生变化。

https://play.golang.org/p/eEChIs0-66G

☛ append 函数

我们可以使用内置函数 append 来向一个切片添加新的元素。 append 函数的格式是:

func append(slice []Type, elems ...Type) []Type

它的第一个参数是一个 slice , 其他一个或多个参数是要往切片中添加的元素,该函数返回一个和 slice 数据类型一致的新的切片。因此, slice 是一个 variadic function (我们会在接下来的教程中学习到更多关于可变参数函数的知识)。

既然 append 函数不会改变原来的slice,让我们来看看它是如何工作的。

https://play.golang.org/p/dSA5x7TkFeS

通过上面的运行结果,我们可以看到, s 没有被改变,两个新的元素被拷贝到了 newS 中,但是,我们可以看到数组 a 发生了变化! append 函数改变了 s 切片所引用的数组。

这太可怕了,因此操作 切片 时要特别注意,在使用 append 函数时,尽量像 s = append(s, ...) 这样,对切片本身进行操作,增加代码的可控性。

如果我们在一个切片中添加足够多的元素,直到超过了切片的容量,将会发生什么? 我们来试一试。

https://play.golang.org/p/qKtVAka498Z

首先,我们创建一个 int 类型的数组 a ,并用一组整数进行初始化。然后,使用数组 a 索引  2 到索引 3 之间的元素来创建一个切片 s 。

在第一段 Print 语句中,我们验证了 s 和 a 的值。然后,我们通过匹配它们各自元素的内存地址来确保 s 引用了数组 a 。切片 s 的长度和容量也得到了确认。

然后我们向切片 s 中再添加 7 个元素,我们预计 s 会有 9个元素,长度为 9 ,但是我们暂时还不知道它的容量将会是多大。在接下来的 Print 语句中,我们发现,切片 s 的容量由原来的 7 变成了 14 ,同时长度变成了 9。但是数组 a 没有被改变。

这看起来有点不可思议。当我们在切片中追加元素时,如果切片引用的数组不足以存储新的元素,go就会创建一个更大的数组并把原来的 slice 的值复制进去,然后将 append 的值也添加的新的数组中去,原来的数组不发生任何变化。

☛ anonymous array 切片

迄今为止,我们看到的切片都是引用了一个明确定义的数组,但大多数情况下,你都是与匿名数组打交道。

和数组类似,slice 也可以使用初始值来定义。在本例中,go将先创建包含这些值的匿名数组,然后创建一个相关切片。

https://play.golang.org/p/l_uhlR5KjNY

很明显,这个切片的容量是 6 ,go会优先创建长度为 6 的数组因为我们将要创建有 6 个元素的切片。那么,当我们添加两个更多的元素时会发生什么?

https://play.golang.org/p/dmcnLc6Ys8c

这时,go创建了一个长度为 12 的数组,因为当我们将 2 个新元素添加到切片时,原来长度为 6 的数组,已经不足以存储  8 个元素。如果我们再将新的元素添加到切片,那么直到切片的长度超过 12 ,都不会再有新的数组被创建。

☛ copy function

go提供了一个内置函数 copy 来拷贝一个切片到另一个切片。copy 函数的格式如下:

func copy(dst, src []Type) int

dst 是目标切片,src 是源切片. copy 函数将会返回拷贝的元素个数,也是 len(dst)len(src) 的最小值。

https://play.golang.org/p/MkFRMZl-v1B

在上面的程序中,我们定义了一个 nil 切片 s1 ,和两个非空的切片 s2 和 s3。第一个 copy 语句试图将 s2 复制到 s1 ,但因为 s1 是一个空切片,所以不会发生任何改变, s1 的值将仍为 nil

append 不会是这种情况。因为go已经准备好在需要时创建新的数组,所以在 nil 的切片中 append 将正常工作。

在第二个 copy 语句中,我们要把 s3复制到 s2 中,因为 s3 包含 4 个元素, s2 包含 3 个元素,所以只有 3 个会被复制(3 和 4 的最小值)。 因为 copy 不会添加新的元素。

在第三个 copy 语句中,我们把 s4 复制到 s3 中。由于 s4 包含 3 个元素,而 s3 包含 4 个元素,因此在  s3 中只会替换 3 个元素。

☛ make function

在上面的例子中,我们看到 s1 保持不变因为它是一个 nil 切片。但是nil slice 和 empty slice 之间存在差异。nil 切片是引用了一个 不存在的数组 ,而 empty 切片是引用了一个 空数组 或其引用的数组变为了空。

make 是一个内置函数,它可以帮你创建空切片。make函数的格式如下。make 函数可以创建很多空复合类型。

func make(t Type, size ...IntegerType) Type

在创建 slice 时,make 函数如下所示:

s := make([]type, len, cap)

这里的 type 是切片中元素的类型,len 是切片的长度,cap 是切片的容量。.

让我们来试试前边的例子,其中 s1 是空切片。

https://play.golang.org/p/z0tlrRYLhMu

上面的结果可以证明,一个空切片被创建,同时即使切片的容量较大, copy 函数也不会向切片添加超出其长度的值。

☛ Type... unpack 操作符

有些人叫它 unpack 操作符或 expand 操作符,我更愿意叫它 spread,感觉更合乎语义。我们已经知道了 append 函数的作用是,可以将多个元素添加到切片中。如果需求是将一个切片中的元素添加到另一个切片中呢?在这种情况下,我们就需要用到 ... 操作符,因为 append 函数不能接收切片作为被添加的元素,但是可以将切片以数据类型的方式作为参数传递。

https://play.golang.org/p/JfLgynyqVYc

☛ [start:end] 提取操作符

go 中有一个非常棒的操作符 [start:end](我喜欢称之为 提取 操作符 ),我们可以用它来提取切片的任何部分。 startend 都是可选的索引。 start 是切片的初始索引,end 表示提取到哪个索引结束,因此提取的内容不包括 end 索引。这样操作会返回一个新切片

https://play.golang.org/p/lNhNx5KGVrR

在上面的例子中,我们创建了一个从 09 的整型切片 s

  • s [:] 表示从索引 0 开始提取切片 s 内的所有元素。因此返回切片 s 的所有元素。

  • s[2:] 表示从索引 2 开始提取切片 s 内的元素。因此返回 [2 3 4 5 6 7 8 9]

  • s[:4] 表示从索引 0 开始,到索引 4 为止,提取切片 s 内的元素,其中,不包括索引 4 对应的元素。因此返回 [0 1 2 3]

  • s[2:4] 表示从索引 2 开始,到索引 4 为止,提取切片 s 内的元素,其中,不包括索引 4 对应的元素。 因此返回 [2 3]

要强调的一点是,由提取运算符创建的所有切片,底层引用的都是同一个数组。 可以使用 copymakeappend 函数来避免这个问题。

☛ Slice 遍历

对于 arrayslice 的遍历是一样的。实际上,slice 就像一个具有相同结构的arrayarray 遍历的所有方法都可以用于 slice

☛ 引用传递

尽管我们说切片是值传递,但由于它是对同一个底层数组的引用,所以它更像是引用传递。

https://play.golang.org/p/p6O0Uqeww1g

在上面的例子中,我们定义了 makeSquares 方法,它接收一个切片参数,并将切片中的元素做平方运算。这将产生以下结果

[0 1 4 9 16 25 36 49 64 81]

这证明了尽管 slice 是值传递,但是由于本质上它是对同一个底层 array 的引用,因此,我们可以更改这个数组中元素的值。

为什么我们如此确定 slice 是值传递,如果把 makeSquares 函数改成 func makeSquares(slice [] int){slice = slice [1:5]},可以看到此函数并不会改变 main 函数中 s 的值。

如果我们把上面程序中的参数改为 array,看看会发生什么。

https://play.golang.org/p/qE8grYQ8Q0s

以上程序的输出为 [0 1 2 3 4 5 6 7 8 9],这意味着 makeSquares 函数只是拿到了数组参数的副本。

☛ 删除 切片 中的元素

go 没有提供任何的关键字或函数来直接删除slice 元素。我们需要使用一些技巧来实现。从切片中删除元素就像在需要删除的元素后面和前面连接切片一样。让我们来看看是如何实现的。

https://play.golang.org/p/LfLGN2m-uSm

在上面的程序中,我们从s中的索引0开始从s中提取了一个切片,但不包括索引2,并从s中提取索引3开始到末尾的元素来追加切片。这将创建没有索引“2”的新切片。上面的程序将打印[0 1 3 4 5 6 7 8 9]。使用相同的技术,我们可以从切片中的任何位置删除多个元素。

☛ 对 slice 进行比较运算

如果尝试运行下面的程序

https://play.golang.org/p/kZ7-SyCBvpt

程序会报错 invalid operation: s1 == s2 (slice can only be compared to nil)。也就是说,我们只能从整体上判断切片的值是否为 nil。如果真的需要比较两个切片,可以使用 for range 循环来分别比较切片中的每个元素。

多维切片

和数组类似,切片也可以是多维的。除了没有指明元素个数,定义多维切片与定义多维数组的语法是非常相似的。

s1 := [][]int{
    []int{1, 2},
    []int{3, 4},
    []int{5, 6},
}

s2 := [][]int{
    {1, 2},
    {3, 4},
    {5, 6},
}

☛ 内存优化

正如我们所知,slice 引用的是一个数组。如果有一个函数返回一个切片, 这个切片可能引用一个大的数组.。只要该切片存在内存中, 就不能对数组进行垃圾回收并且占用很大的系统内存。.

下面是一个糟糕的程序

package main

import "fmt"

func getCountries() []string {
    countries := []string{"United states", "United kingdom", "Austrilia", "India", "China", "Russia", "France", "Germany", "Spain"} // can be much more

    return countries[:3]
}

func main() {
    countries := getCountries()

    fmt.Println(cap(countries)) // 9
}

我们知道, countries 的容量是  9意味着 下面的 数组 包含 9 个 元素 (在这个列子中我们是知道的).

为了避免写出上面糟糕的代码(函数返回的切片引用了一个比较大的数组,占用了较多的内存),我们必须创建一个新的匿名数组切片,使其长度固定。下面的程序是个不错的方案。

package main

import "fmt"

func getCountries() (c []string) {
    countries := []string{"United states", "United kingdom", "Austrilia", "India", "China", "Russia", "France", "Germany", "Spain"} // can be much more

    c = make([]string, 3) // made empty of length and capacity 3
    copy(c, countries[:3]) // copied to `c`

    return
}

func main() {
    countries := getCountries()

    fmt.Println(cap(countries)) // 3
}

☛ 阅读更多

正如你所见,go没有提供类似JavaScript中奇特的函数和方法来操作go的切片。我们使用技巧来删除切片元素。如果你正在寻找像pop(从末尾移除),push(从末尾添加),shift(从头部添加)等功能的实现,不妨参考这个网址的内容 :https://github.com/golang/go/wiki/SliceTri...

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://medium.com/rungo/the-anatomy-of-...

译文地址:https://learnku.com/go/t/28773

本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!