失眠网,内容丰富有趣,生活中的好帮手!
失眠网 > Golang slice原理

Golang slice原理

时间:2020-07-31 21:40:39

相关推荐

Golang slice原理

数组

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

数组类型定义了长度和元素类型。例如,[4]int 类型表示一个由四个整数组成的数组。数组的大小是固定的,长度是数组类型的一部分( [4]int 和 [5]int 是完全不同的类型)。数组可以以常规的索引方式访问,表达式 s[n] 访问数组 s 的第 n 个元素。

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

数组不需要显式的初始化;数组中的零值可以直接使用,数组元素会自动初始化为零值:

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

类型 [4]int 在内存中用四个连续的整数表示:

Go 语言中,数组是值(而不是 C 语言中的指针)。数组变量表示的是整个数组,而不是指向第一个元素的指针(不像 C 语言的数组)。当分配或传递数组值的时候,实际上会复制整个数组。(为了避免复制数组,可以传递一个指向数组的指针,但是数组指针并不是数组。) 可以将数组看作一个特殊的结构体 struct,只是数组中使用索引而不是字段名,同时数组大小固定。

数组的字面值像这样:

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

当然,也可以让编译器统计数组中元素的数目:

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

这两种写法, b 都是 [2]string 类型。

slice

数组虽然有合适的用途,但不够灵活,因此在 Go 代码中并不常用。但是,slice 则使用得相当广泛。slice 基于数组构建,但是提供更强的功能和便利性。

slice 类型的写法是 []T,其中 T 是 slice 中元素的类型。和数组不同的是,slice 类型的长度不固定。

slice 的字面值和数组字面值很像,只是 slice 没有指定元素个数:

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

slice 可以使用内置的 make 函数创建,make 函数的函数声明为:

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

其中 T 表示被创建的 slice 元素的类型。函数 make 接受类型 T、长度 len 和可选的容量 cap 参数。make 会在调用时会分配一个数组,然后返回跟这个数组相关联的 slice。

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

当容量参数被忽略时,默认与长度参数相同。下面的写法中,容量与长度都是 5:

s := make([]byte, 5)

可以使用内置函数 len 和 cap 查看 slice 的长度和容量信息。

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

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

slice 的零值为 nil。对于零值的 slice, len 和 cap 都将返回 0。

也可以对已有的 slice 或数组进行切片生成新的 slice。切片范围由两个由冒号分割的索引对应的左闭右开区间指定。例如,表达式 b[1:4] 创建的 slice 包含数组 b 的第 1 到 3 个元素空间(对应新生成的 slice 的索引 0 到 2)。

b := []byte{'g', 'o', 'l', 'a', 'n', 'g'}// b[1:4] == []byte{'o', 'l', 'a'}, 与数组 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

slice内部

slice 是一个数组片段的描述符号。slice 包含了指向数组的指针,片段长度和容量(片段的最大长度)。

前面使用 make([]byte, 5) 创建的 slice 变量 s 的结构类似这样:

长度是 slice 中引用的元素个数。容量是底层数组的元素个数(从 slice 指针指向的元素开始计算)。长度和容量的区别将在后面几个例子中说明。

继续对 s 进行切片,观察 slice 的数据结构及它引用的底层数组中的变化:

s = s[2:4]

切片操作并不复制 slice 的数据。它创建一个新的 slice 指向原始 slice 对应的底层数组。这使得切片操作与操作数组索引一样高效。因此,修改新 slice 的元素会影响到原始 slice 的对应元素。

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)]

slice 的增长不能超出其容量。就像 slice 或数组的下标越界会引起异常一样,超出 slice 容量的增长也会导致运行时异常。同样,对 slice 再次切片时,不能使用小于零的索引访问 slice 之前的元素。

不断增长的slice(copy和append函数)

要增大 slice 的容量,必须创建一个新的、更大容量的 slice,然后将原有 slice 的内容复制到新 slice。这个技术也是其他语言中动态数组的底层实现方式。下面的例子通过创建新 slice t 将 slice s 的容量翻倍,复制 s 的元素到 t,然后将 t 赋值给 s:

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

这个常见的循环复制操作可以由内置的 copy 函数替代。copy 函数将源 slice 的数据复制到目标 slice,并返回复制元素的个数。

func copy(dst, src []T) int

copy 函数支持不同长度的 slice 之间的复制(只复制较少的元素个数,即较短 slice 的长度)。此外, copy 函数可以正确处理源和目的 slice 共享同一个底层数组的情况,正确处理重叠的 slice。

通过 copy 函数,可以简化上面的代码片段:

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

将数据追加到 slice 是常见操作。下面的函数将字节元素追加到字节类型的 slice, 必要的话会增加 slice 的容量,最后返回更新的 slice:

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}

类似 AppendByte 的函数提供对切片容量增长方式的完全控制,比较实用。根据程序的特点,可能需要分配更小或更大的块,或者对重新分配的大小设置上限。

但大多数程序不需要完全的控制,因此 Go 提供了一个内置函数 append,可以满足大多数场景;函数声明如下:

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

append 函数将 x 追加到 slice s 的末尾,并且在必要的时候增加 slice 的容量。

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

要将 slice 追加到另一个 slice,需要使用 … 语法将第 2 个参数展开为参数列表。

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 切片用起来就像一个长度为零的切片,可以声明一个 slice 变量然后在循环中追加数据:

// Filter returns a new slice holding only// the elements of s that satisfy f()func Filter(s []int, fn func(int) bool) []int {var p []int // == nilfor _, v := range s {if fn(v) {p = append(p, v)}}return p}

可能的“陷阱”

正如前面所说,对 slice 再次切片不会复制底层数组。整个数组将被保存在内存中,直到不再被引用。这可能导致程序只需要一小段数据的时候,将所有数据保存在内存中。

例如,FindDigits 函数加载一个文件到内存中,搜索第一组连续的数字,以切片方式返回。

var digitRegexp = regexp.MustCompile("[0-9]+")func FindDigits(filename string) []byte {b, _ := ioutil.ReadFile(filename)return digitRegexp.Find(b)}

这段代码返回的 []byte 指向保存整个文件的数组。因为 slice 引用了原始的数组并且 slice 一直在使用, 导致 GC 不能释放数组的空间;虽然只用到少数几个字节,但是整个文件的内容都一直保存在内存里。

要解决这个问题,可以将感兴趣的数据复制到一个新的 slice 中:

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

原文链接:/slices-intro

如果觉得《Golang slice原理》对你有帮助,请点赞、收藏,并留下你的观点哦!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。
相关阅读
golang中的slice

golang中的slice

2023-07-11

【Golang】Slice

【Golang】Slice

2019-08-10

golang——slice避坑

golang——slice避坑

2024-01-30

golang slice 详解

golang slice 详解

2023-01-25