数组
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原理》对你有帮助,请点赞、收藏,并留下你的观点哦!