Go语言的切片(slices),可以高效且方便的处理同类型的数据序列
在了解切片之前需要先了解数组
一个数组的定义需要指定长度和元素类型
比如说,[4]int
表示一个存储四个整数的数组。 数组的长度是固定的。长度是数组类型的一部分,因此 [4]int
和 [5]int
是完全不同的类型。 数组可以使用索引方式访问,表达式 s[n]
访问数组的第 n 个元素(从0开始计数)
var a [4]int
a[0] = 1
i := a[0]
// i == 1
在声明数组时,数组元素会自动初始化为其对应类型的零值
// a[2] == 0
数组在内存中是连续存储的,比如说,数组 [4]int
对应内存中四个连续的整数
Go的数组变量表示整个数组,而在C语言中,它是指向数组中第一个元素的指针。这就意味着,当一个数组变量被赋值或者被传递的时候,实际上会复制整个数组。为了避免这样的拷贝,可以利用数组指针进行传递。可以将数组看作一个特殊的结构体,结构的字段名对应数组的索引,同时成员的数目固定
利用字面量声明数组:
b := [2]string{"Penn", "Teller"}
让编译器统计数组字面值中元素的数目:
b := [...]string{"Penn", "Teller"}
这两种写法得到的结果都是一样的
切片是建立在数组之上的抽象数据类型,使用方便,功能强大
切片类型的写法是 []T
, T
是切片元素的类型。和数组不同的是,切片类型并没有固定的长度。
切片的字面量和数组字面量类似,不同的是切片不指定元素个数:
letters := []string{"a", "b", "c", "d"}
切片可以使用内置函数 make
创建:
func make([]T, len, cap) []T
函数 make
参数中,T
表示被创建的切片元素的类型,len
表示长度,cap
表示容量。 调用 make
时,内部会分配一个数组,然后返回数组对应的切片。
var s []byte
s = make([]byte, 5, 5)
// s == []byte{0, 0, 0, 0, 0}
当容量与长度相同时,容量参数可以忽略不写:
s := make([]byte, 5)
可以使用内置函数 len
和 cap
获取切片的长度和容量信息:
len(s) == 5
cap(s) == 5
切片的零值为 nil
。对于切片的零值, len
和 cap
都将返回0
通过现有的数组或则切片可以生成新的切片。切分的范围是一个左闭右开的区间。例如,表达式 b[1:4]
创建的切片引用数组 b
的第2到4个元素空间
b := []byte{'g', 'o', 'l', 'a', 'n', 'g'}
// b[1:4] == []byte{'o', 'l', 'a'}, 共享同一块内存区域
切片的开始和结束的索引都是可选的,它们的默认值分别为零和数组的长度。
// b[:2] == []byte{'g', 'o'}
// b[2:] == []byte{'l', 'a', 'n', 'g'}
// b[:] == b
基于现有数组创建切片:
x := [3]string{"Лайка", "Белка", "Стрелка"}
s := x[:] // 切片引用数组的内存区域
一个切片是一个数组片段的描述
切片包含了指向数组的指针,片段的长度, 和容量(片段的最大长度)
之前使用 make([]byte, 5)
创建的切片变量 s
的结构:
长度是切片引用的元素数目。容量是,从切片指针开始,底层数组的元素数目
我们继续切分切片变量s
:
s = s[2:4]
切片操作将创建一个新的切片,并复用原来切片的底层数组。 这使得切片操作和数组索引一样高效。因此,通过一个新切片修改元素,会影响到原始切片的对应元素
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)]
切片的长度不能超过它的容量。和切片或数组的索引超出范围一样,增长超出切片的范围会导致运行时异常
copy
和append
函数)增加切片的容量,需要创建一个新的、更大容量的切片,然后将原有切片的内容复制到新的切片。 整个技术是一些支持动态数组语言的常见实现。下面的例子将切片s
容量翻倍:
// 创建一个2倍容量的新切片t
t := make([]byte, len(s), (cap(s)+1)*2) // +1操作是处理cap(s)==0的情况
// 复制`s`的元素到`t`
for i := range s {
t[i] = s[i]
}
// 将`t`赋值给`s`
s = t
循环中复制的操作可以由内置函数copy
替代。copy
函数将源切片的元素复制到目的切片。 它返回复制元素的数目
func copy(dst, src []T) int
当源和目的的切片长度不同时,copy
函数只复制,较短切片的长度个元素。 此外, copy
函数可以正确处理源和目的切片有重叠的情况。
使用copy
函数来优化上面将切片s
容量翻倍的例子:
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) { // 需要的空间大于现有的空间
// 为了将来可能的增长,分配更大的内存
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 := make([]int, 1)
// a == []int{0}
a = append(a, 1, 2, 3)
// a == []int{0, 1, 2, 3}
如果是要将一个切片追加到另一个切片尾部,需要使用 ...
语法将第2个参数展开为参数列表。
a := []string{"John", "Paul"}
b := []string{"George", "Ringo", "Pete"}
a = append(a, b...) // 相当于 `a = 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)
}
这段代码执行结果于预期一样,但返回的切片指向了包含了整个文件内容的数组,因此垃圾回收器不能回收这个数组。这就导致了较小有用的的信息却占用了很大的内存空间
我们可以复制有用的信息到一个新的切片里来解决这个问题
func CopyDigits(filename string) []byte {
b, _ := ioutil.ReadFile(filename)
b = digitRegexp.Find(b)
c := make([]byte, len(b))
copy(c, b)
return c
}
https://blog.golang.org/go-slices-usage-and-internals