前言
Go语言中提供了的切片类型,方便使用者处理类型数据序列。
切片有点像其他语言中的数组,并且提供了一些额外的属性。
数组
Go语言自带了数组类型,而切片类型是基于数组类型的抽象。因此,要理解切片类型,我们必须首先理解数组。
定义一个数组时,需要指定数组长度和数组中元素的类型,比如说 [4]int定义了长度为4的数组,其中的元素类型为int。一个数组的长度是固定的;长度是数组类型的一部分([4]int和[5]int就是两个不同的类型)。数组以通常的方式进行索引,所以表达式s[n]能访问到数组s的第n个元素(从0开始)。
var a [4]int
a[0] = 1
i := a[0]
// i == 1
在没有显式初始化时,数组默认会将元素初始化为0。
// a[2] == 0
在内存中,[4]int表示为顺序排列的4个整数值
内存中的 [4]int
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}
如果没有传入cap参数,它的默认值是传入的长度。这是上面代码的一个简洁版本。
s := make([]byte, 5)
可以使用内置的len和cap函数检查切片的长度和容量。
len(s) == 5
cap(s) == 5
下面两个章节将讨论长度和容量的关系。
切片的零值为nil。对一个值为nil的切片来说,len和cap会返回0。
可以通过“切”一个数组或者是切片,来生成新的切片。这个过程通过指定两个索引的半开范围来完成,两个索引之间用冒号隔开。举个例子,b[1:4]会返回一个新的切片,包含的元素为b中的第1到第3的元素
b ;= []byte{'g', 'o', 'l', 'a', 'n', 'g'}
// b[1:4] == []byte{'o', 'l', 'a'} 和b中的元素占用同一块内存
起始和结束索引是可选的,其默认值分别为0和切片的长度
// b[:2] == []byte{'g', 'o'}
// b[2:] == []byte{'l', 'a', 'n', 'g'}
// b[:] == b
基于数组创建切片语法与上面的类似。
x := [3]string{"Лайка", "Белка", "Стрелка"}
s := x[:] // s为指向x的引用
探寻切片内部
切片是数组段的描述符。它包含了一个指向数组的指针,数据段的长度和容量。
切片结构
通过s := make([]byte, 5)方式声明的切片结构如下
s结构
长度是切片指向内容中元素的个数。容量是底层数组中的元素个数(从切片指向的元素开始计数)。长度和容量的区别会在下面的例子中解释。
对s进行切片,观察下面切片和数组的关系
s = s[2:4]
切片和数组
切片操作并不会拷贝s中的数据,而是创建一个新的切片指向原来的数组,这让切片操作就像操作数组索引一样高效。因此,对切片的元素进行修改,会修改原始切片的元素。
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)]
切片后结果
切片的长度不能大于其容量。这样做会导致一个runtime panic,就像对切片或者数组进行越界访问一样。
增加切片容量
要增加切片的容量,必须新建一个容量更大的切片,然后将之前的切片的数据拷贝到新的切片中。这也是其他语言实现动态数组的方式。下面的例子,新建一个容量是s两倍的切片t,然后将s的数据拷贝到t中,最后将t赋值给s:
t := make([]byte, len(s)m (cap(s)+1)*2) // +1对应 cap(s) == 0的情况
for i := range s {
t[i] = s[i]
}
s = t
使用内置的copy函数可以简化上面的代码。顾名思义,copy将数据从一个切片拷贝到另一个切片,并返回拷贝元素的数量。
语法如下:
func copy(dst, src []T) int
函数copy 支持两个不同长度切片之间的拷贝。另外,copy可以处理源和目的切片指向相同底层数组的情况,正确处理重叠的切片。
简化上面的代码
t := make([]byte, len(s), (cap(s)+1)*2)
copy(t, s)
s = t
一个常见的操作是在切片的末尾添加一个元素。下面的函数在一个切片的末尾增加一个元素,在容量不够的情况下增加切片的容量,并且返回更新后的切片
func AppendByte(slice []byte, data ...type) []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函数就派上用场了。它的语法如下
fun append(s []T, x ...T) []T
函数append 将x添加到s末尾,如果需要就扩展s的容量。
a := make([]int, 1)
// a == []int{0}
a = append(a, 1, 2, 3)
// a == []int{0, 1, 2, 3}
使用…将一个切片添加到另外一个切片末尾
a := []string{"John", "Paul"}
b := []string{"George", "Ringo", "Pete"}
a = append(a, b...) // 等同于append(a, b[0], b[1], b[2])
// a == []string{"John", "Paul", "George", "Ringo", "Pete"}
因为零值的切片(nil)和长度为0的切片相似,可以声明一个切片变量,然后在循环中在其末尾添加元素。
// 通过fn筛选出s中的元素
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)
}
上面的代码能完成所需要的功能,但是返回的[]byte切片指向的是保存了文件所有数据的数组。只要这个切片一直保留着,垃圾回收将不能释放保存了所有数据的数组。文件一小部分有用的数据将会让所有的数据一直保存在内存中。
要解决这个问题,可以先将有用的数据先保存到一个新的切片,然后返回新的切片。
func CopyDigits(filename string) []byte {
b, _ := ioutil.ReadFile(filename)
b = digitRegexp.Find(b)
c := make([]byte, len(b))
copy(c, b)
return c
}