dduo518 / hexo-blog

hexo静态blog点击 https://github.com/chong0808/hexo-blog/issues
3 stars 0 forks source link

[译] GO slice: 用法与内部原理 #40

Open dduo518 opened 3 years ago

dduo518 commented 3 years ago

[译] GO slice: 用法与内部原理

原文:Go Slices: usage and internals

介绍

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

数组

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

数组类型定义指定长度和元素类型。例如,类型[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的内存中表示形式只是依次排列的四个整数值: 2021-05-23-slice-array

Go的数组是值。数组变量表示整个数组;不是指向第一个数组元素的指针(就像C中的情况一样)。这意味着当分配或传递数组值时,将对其内容进行复制。(为避免复制,可以将指针传递给数组,但这是指向数组而不是数组的指针.), 有种方法是将数据当做一个有序的结构体,但使用索引字段而不是命名字段:固定大小的复合值。 可以这样显示指定数组长度大小:

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}

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

s := make([]byte, 5)

可以使用内置的len和cap功能检查切片的长度和容量。

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

接下来讨论长度和容量之间的关系。 切片的零值为nillencap函数都将为零切片返回0。 一个切片也可以通过一个切片或者数组截取,通过指定一个半开范围来进行切片,其中两个索引之间用冒号分隔(前开后闭区间) 例如,表达式b[1:4]创建一个包含b的元素1到3的切片(所得切片的索引将为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

切片的内部原理

切片是数组的描述符。它由指向数组的指针、长度及其容量(数组最大长度)组成。 2021-05-23-slice-struct

变量s是由make([]byte,5)创建的,其结构如下: 2021-05-23-slice-1 长度是所指切片的元素个数,容量是基础数组中元素的数量(从切片底层数组指针所指的元素开始) 在接下来的几个示例中,将明确区分长度和容量。 在对s进行切片时,观察slice数据结构中的变化及其与基础数组的关系:

s = s[2:4]

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

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[:cap(s)]

2021-05-23-slice-3 切片不能超出其容量。尝试这样做会导致运行时错误,就像在切片或数组的边界之外进行索引时一样。同样,无法将切片重新切片为零以下以访问数组中较前面的元素。

切片增长(复制与追加)

要增加切片的容量,必须创建一个新的切片、较大的切片,然后将原始切片的内容复制到其中。其他语言的动态数组背后的实现方式,下一个示例通过制作一个新的切片t,将s的内容复制到t,然后将切片值t分配给s,使s的容量增加一倍:

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

内置的复制功能使此普通操作的循环过程变得更容易。 顾名思义,copy方法将数据从源切片复制到目标切片。然后返回复制的元素数。

func copy(dst, src []T) int

复制功能支持在不同长度的切片之间进行复制(它最多只能复制较少数量的元素),此外,复制可以处理共享同一基础数组的源切片和目标切片,从而正确处理重叠的切片。 使用复制,可以简化上面的代码片段:

t := make([]byte, len(s), (cap(s)+1)*2) //t的容量大于s容量的2倍
copy(t, s) // 将是复制到t里面
s = t

常见的操作是将数据附加到切片的末尾。此函数将字节元素附加到字节片上,如有必要,使该片增大,并返回更新后的片值:

func AppendByte(slice []byte, data ...byte) []byte {
    m := len(slice)
    n := m + len(data)
    if n > cap(slice) { // 如果容量较小需要重新分配内存
        // 分配一倍大的内存空间容量
        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}

诸如AppendByte之类的功能很有用,因为它们可以完全控制切片的生长方式。根据程序的特性,可能希望分配更大或更小的块,或者对重新分配的大小设置上限。 但是大多数程序并不需要完全控制,因此Go提供了一个内置的append函数,可以很好地满足大多数目的。它具有方法签名:

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

append函数将元素x附加到切片s的末尾,并在需要更大容量的情况下扩大切片。

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)的作用类似于零长度切片,因此可以声明一个切片变量,然后在循环中附加到该变量:

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
}`

有坑的地方

如前所述,对切片进行重新切片(a[startIndex:endIndex])不会复制基础数组,完整的数组将保留在内存中,直到不再被引用为止。有时,这可能导致程序仅需要一小部分数据时就将所有数据保存在内存中。 例如,此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
}