Go Slices:用法和内部结构


介绍

Go 的 slice 类型提供了一种方便有效的方法来处理类型化数据的序列。切片类似于其他语言中的数组,但具有一些不寻常的属性。本文将介绍切片是什么以及它们是如何使用的。

数组

切片类型是建立在 Go 数组类型之上的抽象,因此要理解切片我们必须先了解数组。

数组类型定义指定长度和元素类型。例如,该类型[4]int表示一个由四个整数组成的数组。数组的大小是固定的;它的长度是其类型的一部分([4]int并且[5]int是不同的、不兼容的类型)。数组可以用通常的方式索引,所以表达式s[n]访问第 n 个元素,从零开始。

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

数组不需要显式初始化;数组的零值是一个随时可用的数组,其元素本身已归零:

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

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

img

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

可以像这样指定数组文字:

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

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

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

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

切片

数组有其一席之地,但它们有点不灵活,因此您不会在 Go 代码中经常看到它们。然而,切片无处不在。它们建立在阵列上,以提供强大的功能和便利。

切片的类型规范是[]T,其中T是切片元素的类型。与数组类型不同,切片类型没有指定的长度。

切片文字的声明就像数组文字一样,只是您省略了元素计数:

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

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

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

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

var s []byte
s = make([]byte, 5, 5)
// s == []byte{0, 0, 0, 0, 0}

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

s := make([]byte, 5)

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

len(s) == 5cap(s) == 5

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

切片的零值是nil。对于 nil 切片,lencap函数都将返回 0。

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

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

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

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

这也是在给定数组的情况下创建切片的语法:

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

切片内件

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

img

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

img

长度是切片引用的元素数。容量是底层数组中的元素数(从切片指针引用的元素开始)。在我们浏览接下来的几个示例时,长度和容量之间的区别将变得清晰。

当我们切片时s,观察切片数据结构的变化及其与底层数组的关系:

s = s[2:4]

img

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

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 的容量:

s = s[:cap(s)]

img

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

增长切片(复制和追加功能)

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

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

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

func copy(dst, src []T) int

copy函数支持在不同长度的切片之间进行复制(它只会复制到较少数量的元素)。此外,copy可以处理共享相同底层数组的源切片和目标切片,正确处理重叠的切片。

使用copy,我们可以简化上面的代码片段:

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

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

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

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

like 函数AppendByte很有用,因为它们可以完全控制切片的生长方式。根据程序的特性,可能需要以较小或较大的块进行分配,或者对重新分配的大小设置上限。

但是大多数程序不需要完全控制,因此 Go 提供了一个append适用于大多数用途的内置函数;它有签名

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

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

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

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

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)的零值就像一个零长度切片,您可以声明一个切片变量,然后在循环中附加到它:

// Filter returns a new slice holding only// the elements of s 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函数将一个文件加载到内存中,并在其中搜索第一组连续数字,并将它们作为新切片返回。

var digitRegexp = regexp.MustCompile("[0-9]+")

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

此代码的行为与广告一样,但返回的[]byte指向包含整个文件的数组。由于切片引用了原始数组,所以只要切片保持在垃圾收集器周围,就无法释放数组;文件的几个有用字节将整个内容保存在内存中。

为了解决这个问题,可以在返回之前将有趣的数据复制到一个新的切片中:

func CopyDigits(filename string) []byte {
    b, _ := ioutil.ReadFile(filename)
    b = digitRegexp.Find(b)
    c := make([]byte, len(b))
    copy(c, b)
    return c
}

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


原文链接:https://codingdict.com/