[toc]
Go的切片类型提供了一个方便和高效的处理类型化数据序列的方法。切片类似于其他语言的数组,但是有一些不常见的特征。这篇文章将介绍切片是什么以及如何使用。
切片类型是一个建在Go数组类型之上的抽象,因为为了理解切片,我们必须理解数组。
一个数组定义了特定的长度和一个元素类型。例如,类型[4]int
表示一个有4个整数的数组。一个数组的大小是固定的。它的长度是它类型的一部分([4]int
和[5]int
是不同的,不相容的类型)。数组能够用常见的方法进行索引,因此表达式s[n]
读取第n个元素,从0开始。
var a [4]int
a[0] = 1
i := a[0]
// i == 1
数组不需要明确的被初始化;一个数组的零值是一个准备去使用的数组,元素本身是零值。
// a[2] == 0 , int类型的0值
在内存中表示[4]int
是依次存放4个整数。
Go的数组是值。数组变量表示整个数组。它并不是一个指向第一个元素的指针(和C的情况一样)。这意味着当你分配或传递一个数组值,你将创建一个内容的拷贝。(为了避免复制,您可以传递一个指向数组的指针,但那是指向数组的指针,而不是数组)。一个思考数组的方法是把它作为有序的结构,但是带有索引而不是带有字段名称:一个固定大小的合成值。
一个数组的字面量,能够被指定像这样:
b := [2]string{"aa", "bb"}
或者,你可以让编译器计算为你元素的数量。
b := [...]string{"aa", "bb"}
这两种情况,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)
一个切片的长度和容量能够被检查,使用内置的len
和cap
函数。
len(s)
cap(s)
接下来两部分讨论长度和容量的关系。
切片的零值是nil。对于一个nil的切片,它的len
和cap
函数都返回0。
一个切片也能通过切片现有的切片或数组来形成。切片被创建通过指定一个带有两个索引的半开的范围,中间用冒号分割。例如,表达式b[1:4]
创建一个切片包含b中从1到3的元素(切片结果的索引将从0到2)。
b := []byte{'g', 'b', '1', 'a', 'n', 'g'}
// b[1:4] == []byte{'b', '1', 'a'} 和b共享相同的存储
一个切片表达式开始和结束的索引是可选的。默认分别是0 和 切片的长度。
// b[:2] == []byte{'g', 'b'}
// b[2:] == []byte{'1', 'a', 'n', 'g'}
// b[:] == b
这也是通过给定数组创建切片的语法:
x := [3]string{"aa", "bb", "cc"}
s := x[x] // 引用 x 的存储的切片
一个切片是一个部分数组的描述符。它由一个指向数组的指针、这部分的长度、和它的容量(最大为这部分的长度)组成,
变量s,最早通过make([]byte, 5)
创建,它的结构像这样:
长度是指向切片元素的数量。容量是内部数组的元素数量(从切片指针引用的位置开始)。
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 = s[:cap(s)]
一个切片不能增长超过它的容量。尝试这样做将造成一个运行时的恐慌,就像当索引超过切片或数组的边界。切片不能在零以下重新切片以访问数组中较早的元素。
为了让切片的容量增加,必须创建一个新的、更大的切片,并拷贝原始切片的内容到它里面。这个技术是其它语言动态数组实现在幕后工作方式。下面的例子,让s
的容量翻倍,通过创建一个新的切片,t,拷贝s的内容到t中。然后分配切片t的值给s。
t := make([]byte, len(s), (cap(s)+1)*2) // +1 用于cap(s)==0 的情况
for i := range(s) {
t[i] = s[i]
}
s = t
内置的复制功能使这种常见操作的循环片段变得更容易。 顾名思义,复制将数据从源切片复制到目标切片。 它返回复制的元素数。
func copy(dst, src []T) int
这个拷贝函数支持拷贝不同切片的长度(它将仅仅拷贝较小数量的元素)。除此之外,copy
能够处理源和目标切片共享相同的内部数组,从而正确处理重叠切片。
使用copy
你能简化代码片段:
t := make([]byte, len(s), (cap(s)+1)*2)
copy(t, s)
s = t
一个常用操作是添加数据到切片的结尾。这个函数添加byte元素到一个类型为byte到切片,如果需要增长切片,返回更新的切皮值。
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}
为了添加一个切片到另一个里面,使用...
去扩展第二个参数成为参数列表。
a := []string{"aa", "bb"}
b := []string{"cc", "dd", "ee"}
a = append(a, b...) // 等价于 "append(a, b[0], b[1], b[2])"
// a == []string{"aa", "bb", "cc", "dd", "ee"}
因为一个切片到零值(nil)看起来像一个零长度的切片,你能声明一个切片变量,并且循环向它里面添加。
// 过滤并返回一个新的切片,仅仅持有s中满足fn()的元素
func Filter(s []int, fn func(int) bool) []int {
var p []int
for _, v := range s {
if fn(v) {
p = append(p, v)
}
}
return p
}
正如早期提到,重新对一个切片进行切片,并不会对底层数组进行拷贝。完整的数组将包存在内存中,直到不再有引用。偶尔,这能造成问题对于持有所有数据在内存中,而只有很小一片的数据需要。
例如,这个FindDigits
函数加载文件到内存中,并搜索出第一组连续的数字,把它们作为新的切片返回。
var digitRegexp = regexp.MustCompile("[1-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
进行构造。
func CopyDigits(filename string) []byte {
b, _ := ioutil.ReadFile(filename)
b = digitRegexp.Find(b)
return append(make([]byte,0), b...)
}