3. Go语言数据类型
上篇我们介绍了Go语言的数组类型,本篇将介绍Go语言的切片类型。主要如下:
3.3 切片
切片可以看作是对数组的一种包装形式。切片包装的数组称为该切片的底层数组。切片是针对其底层数组中某个连续片段的描述符。
3.3.1 类型表示法
对于一个元素类型为T的切片类型来说,它的类型字面量就是:
[]T
可以看出,长度并不是切片类型的一部分(即它不会出现在表示切片类型的类型字面量中)。另外,切片的长度是可变的。相同类型的切片值可能会有不同的长度。
切片类型声明中的元素类型也可以是任意一个有效的Go语言数据类型。例如:
[]rune
如上用于表示元素类型为rune的切片类型。
同样可以把一个匿名结构体类型作为切片类型的元素类型。例如:
[] struct {name, department string}
3.3.2 值表示法
和数组类似,也是复合字面量中的一种,例如:
[]string{"Go", "Python", "Java", "C", "C++", "PHP"}
在切片值所属的类型中根本就没有关于长度的规定。以下切片是合法的:
[]string{8: "Go", 2: "Swift", "Java", "C", "C++", "PHP"}
上面的等同于下面的复合字面量:
[]string{0: "", 1: "", 2: "Swift", 3: "Java", 4: "C", 5: "C++", 6: "PHP", 7: "", 8: "Go"}
3.3.3 属性和基本操作
切片类型的零值为nil。在初始化之前,一个切片类型的变量值为nil。
切片类型中虽然没有关于长度的声明,但是值是有长度的,体现在它们所包含的元素值的实际数量。可以使用内建函数len来获取切片值的长度。例如:
len([]string{8: "Go", 2: "Swift", "Java", "C", "C++", "PHP"})
上面计算的结果值为9,这个切片值实际包含了6个被明确指定的string类型值和3个被填充的string类型的零值“”。
注意:在切片类型的零值(即nil)上应用内建函数len会得到0。
切片值的底层实现方式:
一个切片值总会持有一个对某个数组值的引用。一个切片值一旦被初始化,就会与一个包含了其中元素值的数组值相关联。这个数组值被称为引用他的切片值的底层数组。
多个切片值可能会共用一个底层数组。例如,如果把一个切片值复制成多个,或者针对其中的某个连续片段在切片成新的切片值,那么这些切片值所引用的都会是同一个底层数组。对切片值中的元素值的修改,实质上就是对其底层数组上的对应元素的修改。反过来讲,对作为底层元素的数组值中的元素值的改变,也会体现到引用该底层数组其包含该元素值的所有切片值上。
除了长度之外,切片值还有一个很重要的属性-----容量。切片值的容量与它所持有的底层数组的长度有关。可以通过内建函数cap来获取它。例如:
cap([]string{8: "Go", 2: "Swift", "Java", "C", "C++", "PHP"})
该切片值的容量是9,就等于它的长度。这是个特例,但很多情况下不是这样,且听慢慢道来。
切片值的底层数据结构:
一个切片值的底层数据结构包含了一个指向底层数组的指针类型值,一个代表了切片长度的int类型值和一个代表了切片容量的int类型值。
可以使用切片表达式从一个数组值或者切片值上”切”出一个连续片段,并生成一个新的切片值。例如:
array1 := [...]string{8: "Go", 2: "Swift", "Java", "C", "C++", "PHP"}
slice1 := array1[:4]
变量slice1的值的底层数组实际上就是变量array1的值,如下图:
经过上面的描述,大家可能认为一个切片的容量可能就是其底层数组的长度。但事实并非如此,这里再创建一个切片值。例如:
slice2 := array1[3:]
变量slice2的值的底层数组也是变量array1的值,如下图:
如上所示slice2的值的容量与array1的值的长度并不相等。实际上,一个切片值的容量是从其中的指针指向的那个元素值到底层数组的最后一个元素值的计数值。切片值的容量的含义是其能够访问到的当前底层数组中的元素值的最大数量。
可以对切片值进行扩展,以查看更多底层数组元素。但是,并不能直接通过再切片的方式来扩展窗口。例如对于上面原始的slice1的值进行如下操作:
slice1[4]
这会引起一个运行时恐慌,因为其中的索引值超出了这个切片值当前的长度,这是不允许的。正确拓展的方式如下:
slice1 = slice1[:cap(slice1)]
通过再切片的方式把slice1扩展到了最大,可以看到最多的底层数组元素值了。这时slice1的值的长度等于其容量。
注意:一个切片值的容量是固定的。也就是说,能够看到的底层数组元素的最大数量是固定的。
不能把切片值扩展到其容量之外,例如:
slice1 = slice1[:cap(slice1)+1]//超出slice1容量的范围,这样会引起一个运行时恐慌
一个切片值只能向索引递增的方向扩展。例如:
slice2 = slice2[-2:] //这会引起一个运行时恐慌。另外,切片值不允许由负整数字面量代表。
使用append函数来扩展一开始的slice1的值:
slice1 = append(slice1, "Ruby", "Erlang")
执行该语句后,切片类型变量slice1的值及其底层数组(数组变量array1的值)的状态,如下图:
可以看出,slice1的值的长度已经由原来的4增长到了6,与它的容量是相同的。但是由于这个值的长度还没有超出它的容量,所以没必要再创建出一个新的底层数组出来。
原来的slice1的值为:
[]string{"Go", "Python", "Java", "C"}
现在的slice1的值为:
[]string{"Go", "Python", "Java", "C", "Ruby", "Erlang"}
原来的array1的值为:
[6]string{"Go", "Python", "Java", "C", "C++", "PHP"}
现在的array1的值为:
[6]string{"Go", "Python", "Java", "C", "Ruby", "Erlang"}
对现在的slice1再进行扩展,如下:
slice1 = append(slice1, "Lisp")
执行这条语句后,变量slice1的值的长度就超出了它的容量。这时将会有一个新的数组值被创建并初始化。这个新的数组值将作为在append函数新创建的切片值的底层数组,并包含原切片值中的全部元素值以及作为扩展内容的所有元素值。新切片值中的指针将指向其底层数组的第一个元素值,且它长度和容量都与其底层数组的长度相同。最后,这个新的切片值会被赋给变量slice1。
可以使用append函数把两个元素类型相同的切片值连接起来。例如:
slice1 = append(slice1, slice...)
当然也可以把数组值作为第二个参数传递给append函数。
即使切片类型的变量的值为零值nil,也会被看作是长度为0的切片值。例如:
slice2 = nil
slice2 = append(slice2, slice1...)
或者如下:
var slice4 []string
slice4 = append(slice4, slice...)
上面第一条语句用于声明(不包含初始化)一个变量。以关键字var作为开始,并后跟变量的名称和类型。未被初始化的切片变量的值为nil。
3.3.4 切片使用的复杂用法
切片表达式中添加第三个索引-----容量上界索引。如果被指定,那么切片表达式的求值结果的那个切片值的容量就不再是该切片表达式的操作对象的容量与该表达式中的元素下界索引之间的差值,而是容量上界索引与元素下界索引之间的差值。
指定容量上界索引的目的就是为了减小新切片值的容量,可以允许更加灵活的数据隔离策略。
var array2 [10]int = [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
slice5 := array2[2:6]
如上我们可以直接访问和修改array2中对应索引值在[2,6)范围之内的元素值。
slice5 = slice5[:cap(slice5)]
如上再切片后,可以访问和修改array2的值中对应索引值在[2,10)范围之内的元素值。
如果slice5的值作为数据载体传递给了另一个程序,那么这个程序可以随意地更改array2的值中的某些元素值。这就等于暴露了程序中的部分实现细节,并公开了一个可以间接修改程序内部状态的方法,而往往这并不是我们想要的。
如果这样声明slice5:
slice5 := array2[2:6:8]
这样slice5的持有者只能访问和修改array2的值中对应索引值在[2,8)范围之内的元素值。
slice5 = slice5[:cap(slice5)]
即使将slice5扩展到最大,也不能通过它访问到array2的值中对应索引值大于等于8的那些元素。此时,slice5的值的容量为6(容量上界索引与元素下界索引的差值)。对于切片操作来说,被操作对象的容量是一个不可逾越的限制。slice5的值对其底层数组(array2的值)的“访问权限”得到了限制。
如果在slice5的值之上的扩展超出了它的容量:
slice5 = append(slice5, []int{10, 11, 12, 13, 14, 15}…)
那么它原有的底层数组就会被替换。也就彻底切断了通过slice5访问和修改其原有底层数组中的元素值的途径。
切片表达式中的3个索引的限制:当在切片表达式中指定容量上界索引的时候,元素上界索引是不能够省略。但是,在这种情况下元素下界索引却是可以省略的。例如:
slice5[:3:5]//合法的切片表达式
slice5[0::5]//非法的切片表达式,会造成一个编译错误
批量复制切片值中的元素
sliceA := []string{"Notepad", "UltraEdit", "Eclipse"}
sliceB := []string{"Vim", "Emacs", "LiteIDE", "IDEA"}
使用Go语言的内建函数copy,将变量sliceB的值中的元素复制到sliceA的值中。例如:
n1 := copy(sliceA,sliceB)
内建函数copy的作用是把源切片值(第二个参数值)中的元素值复制到目标切片值(第一个参数值)中,并且返回被复制的元素值的数量。copy函数的两个参数的元素类型必须一致,且它实际复制的元素值的数量将等于长度较短的那个切片值的长度。
变量n1的值为3, 变量sliceA的值被修改为:
[]string{"Vim", "Emacs", "LiteIDE"}