首页 [译]Go Slices:使用以及原理/Go Slices: usage and internals
文章
取消

[译]Go Slices:使用以及原理/Go Slices: usage and internals

refer: Go Slices: usage and internals

介绍

Go的 slices 类型在序列类型上提供一个便捷并且高效的工作方式。Slices类似于其他语言的数组,但是有一些特别的属性。这篇文章将会看看什么是slices和它如何使用。

数组

slices类型是一个抽象建立于Go的基类数组类型,所以想要明白slices我们必须首先明白数组。 一个数组类型定义了指定长度和元素类型,例如,类型 [4] int 表示四个整形。数组的大小是固定的;这个类型的长度( [4] int[5] int 是不同的,不兼容的类型)。数组可以按照通常方式被索引。所以这个表达式 s[n] 可以访问第n个元素,索引号从0开始。

1
2
3
4
var a [4]int
a[0] = 1
i := a[0]
// i == 1

数组不需要明确初始化;数组的零值是一个现成的数组,这些数组值都被赋予了对应类型的零值。

1
// a[2] == 0, the zero value of the int type

[4]int 的内存表示只是四个按顺序排列的整数值

01

Go的数组是值。 数组变量表示整个数组;它不是指向第一个数组元素的指针(就像 C 中的情况一样)。这意味着当您分配或传递数组值时,您将复制其内容。(为了避免复制,你可以传递一个指向数组的指针,但那是指向数组的指针,而不是数组。)考虑数组的一种方法是将其视为一种结构,但具有索引而不是命名字段:固定大小的复合值。 可以像这样指定数组文字:

1
b := [2]string{"Penn", "Teller"}

或者,您可以让编译器为您计算数组元素:

1
b := [...]string{"Penn", "Teller"}

在这两种情况下,b 的类型都是 [2]string

切片

数组有它们的位置,但它们有点不灵活,所以你不会经常在 Go 代码中看到它们。 然而,切片无处不在。 它们建立在阵列之上,以提供强大的功能和便利性。 切片的类型规范是 []T,其中 T 是切片元素的类型。 与数组类型不同,切片类型没有指定长度。 切片文字的声明方式与数组文字一样,只是省略了元素计数:

1
letters := []string{"a", "b", "c", "d"}

可以使用名为 make 的内置函数创建切片,该函数具有签名,

1
func make([]T, len, cap) []T

其中 T 代表要创建的切片的元素类型。 make 函数接受类型、长度和可选容量。 调用时,make 分配一个数组并返回一个引用该数组的切片。

1
2
3
var s []byte
s = make([]byte, 5, 5)
// s = []byte{0, 0, 0, 0, 0)

当容量参数被省略时,它默认为指定的长度。 这是相同代码的更简洁版本:

1
s := make([]byte, 5)

可以使用内置的 lencap 函数检查切片的长度和容量。

1
2
len(s) == 5
cap(s) == 5

接下来两个小节讨论长度和容量的关系。

slices的零值是 nilnil的切片的lencap函数返回是0。 切片也可以通过“切片”现有切片或数组来形成。 切片是通过用冒号分隔的两个索引指定一个半开范围来完成的。 例如,表达式 b[1:4] 创建一个包含 b 的元素 1 到 3 的切片(结果切片的索引将是 0 到 2)。

1
2
b := []byte{'g', 'o', 'l', 'a', 'n', 'g'}
// b[1:4] == []byte{'o', 'l', 'a'}, sharing the same storage as b

切片表达式的开始和结束索引是可选的; 它们分别默认为零和切片的长度:

1
2
3
// b[:2] == []byte{'g', 'o'}
// b[2:] == []byte{'l', 'a', 'n', 'g'}
// b[:] == b

这也是给定数组创建切片的语法:

1
2
x := [3]string{"Лайка", "Белка", "Стрелка"}
s := x[:] // a slice referencing the storage of x

切片原理

切片是数组段的描述符。 它由指向数组的指针、段的长度及其容量(段的最大长度)组成。

02

我们之前由 make([]byte, 5) 创建的变量 s 的结构如下:

03

长度是切片引用的元素数。 容量是底层数组中元素的数量(从切片指针引用的元素开始)。 当我们通过接下来的几个示例时,长度和容量之间的区别将变得清晰。 当我们对 s 进行切片时,观察切片数据结构的变化以及它们与底层数组的关系:

1
s = s[2:4]

04

切片不会复制切片的数据。 它创建一个指向原始数组的新切片值。 这使得切片操作与操作数组索引一样高效。 因此,修改重新切片的元素(而不是切片本身)会修改原始切片的元素:

1
2
3
4
5
6
d := []byte{'r', 'o', 'a', 'd'}
e := d[2:]
// e == []byte{'a', 'd'}
e[1] = 'm'
// e == []byte{'a', 'm'}
// d == []byte{'r', 'o', 'a', 'm'}

之前我们将 s 切成比其容量更短的长度。 我们可以通过再次切片来增加 s 的容量:

1
s = s[:cap(s)]

切片不能超出其容量。 尝试这样做会导致运行时异常,就像在切片或数组的边界之外进行索引一样。 同样,切片不能在零以下重新切片以访问数组中较早的元素。

增长切片(拷贝和追加功能)

要增加切片的容量,必须创建一个新的、更大的切片并将原始切片的内容复制到其中。 这种技术是其他语言的动态数组实现在幕后工作的方式。 下一个示例通过创建一个新切片 ts 的容量翻倍,将 s 的内容复制到 t 中,然后将切片值 t 分配给 s

1
2
3
4
5
t := make([]byte, len(s), (cap(s) + 1) * 2) // +1 in case cap(s) == 0
for i := range s {
    t[i] = s[i]
}
s = t

内置的复制功能使这种常见操作的循环片段变得更容易。 顾名思义,复制将数据从源切片复制到目标切片。 它返回复制的元素数。

1
func copy(dst, src []T) int

复制功能支持在不同长度的切片之间复制(它只会复制到较少数量的元素)。 此外,复制可以处理共享相同底层数组的源切片和目标切片,从而正确处理重叠切片。 使用 copy ,我们可以简化上面的代码片段:

1
2
3
t := make([]byte, len(s), (cap(s)+1)*2)
copy(t, s)
s = t

一个常见的操作是将数据附加到切片的末尾。 此函数将字节元素附加到字节切片,必要时增加切片,并返回更新后的切片值:

1
2
3
4
5
6
7
8
9
10
11
12
13
func AppendByte(slice []byte, data ...byte) []byte {
    m := len(slice)
    n := m + len(data)
    if n > cap(slice) { // if necessary, reallocate
        // allocate double what's needed, for future growth
        newSlice := make([]byte, (n+1)*2)
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[0:n]
    copy(slice[m:n], data)
    return slice
}

可以像这样使用 AppendByte

1
2
3
p := []byte{2, 3, 5}
p = AppendByte(p, 7, 11, 13)
// p == []byte{2, 3, 5, 7, 11, 13}

AppendByte 这样的函数很有用,因为它们可以完全控制切片的增长方式。 根据程序的特性,可能希望以更小或更大的块进行分配,或者对重新分配的大小设置上限。 但是大多数程序不需要完全控制,所以 Go 提供了一个内置的 append 函数,它适用于大多数用途; 它有签名。

1
func append(s []T, x ...T) []T

append 函数将元素 x 附加到切片 s 的末尾,如果需要更大的容量,则增加切片。

1
2
3
4
a := make([]int, 1)
// a == []int{0}
a = append(a, 1, 2, 3)
// a == []int{0, 1, 2, 3}

要将一个切片附加到另一个切片,请使用 … 将第二个参数扩展为参数列表。

1
2
3
4
a := []string{"John", "Paul"}
b := []string{"George", "Ringo", "Pete"}
a = append(a, b...) // equivalent to "append(a, b[0], b[1], b[2])"
// a == []string{"John", "Paul", "George", "Ringo", "Pete"}

由于切片的零值 (nil) 的作用类似于长度为零的切片,因此您可以声明切片变量,然后在循环中附加到它:

1
2
3
4
5
6
7
8
9
10
11
// Filter returns a new slice holding only
// the elements of a that satisfy fn()
func Filter(s []int, fn func(int) bool) []int {
    var p []int // == nil
    for _, v := range s {
        if fn(v) {
            p = append(p, v)
        }
    }
    return p
}

一个可能的“陷阱”

如前所述,重新切片切片不会复制底层数组。 完整的数组将保存在内存中,直到不再被引用。 有时,这可能会导致程序在只需要一小部分时将所有数据保存在内存中。 例如,这个 FindDigits 函数将一个文件加载到内存中,并在其中搜索第一组连续的数字,并将它们作为新切片返回。

1
2
3
4
5
6
var digitRegexp = regexp.MustCompile("[0-9]+")

func FindDigits(filename string) []byte {
    b, _ := ioutil.ReadFile(filename)
    return digitRegexp.Find(b)
}

此代码的行为与宣传的一样,但返回的 []byte 指向包含整个文件的数组。 由于 slice 引用了原始数组,所以只要将 slice 保留在垃圾收集器周围,就无法释放数组; 文件的几个有用字节将全部内容保存在内存中。 要解决这个问题,可以在返回之前将感兴趣的数据复制到一个新切片中:

1
2
3
4
5
6
7
func CopyDigits(filename string) []byte {
    b, _ := ioutil.ReadFile(filename)
    b = digitRegexp.Find(b)
    c := make([]byte, len(b))
    copy(c, b)
    return c
}

可以使用 append 构造这个函数的更简洁的版本。 这留给读者作为练习。

本文由作者按照 CC BY-NC-SA 4.0 进行授权

shell命令中的2>&1

[译]如何在Go中使用接口/How to use interfaces in Go