Go 语言的切片类型属于引用类型,同属引用类型的还有字典类型、通道类型、函数类型等;而go语言的数组类型属于值类型,同属值类型的有基础数据类型和结构体类型。
Go语言中,判断所谓的“传值”或者“传引用”只要看传递的值的类型就好了。
数组的容量永远等于其长度,都是不可变的。切片的容量并不是这样,且它的变化是有规律可寻的。
package main import "fmt" func main() { // 示例1。 s1 := make([]int, 5) fmt.Printf("The length of s1: %d ", len(s1)) fmt.Printf("The capacity of s1: %d ", cap(s1)) fmt.Printf("The value of s1: %d ", s1) s2 := make([]int, 5, 8) fmt.Printf("The length of s2: %d ", len(s2)) fmt.Printf("The capacity of s2: %d ", cap(s2)) fmt.Printf("The value of s2: %d ", s2) }
输出:
The length of s1: 5 The capacity of s1: 5 The value of s1: [0 0 0 0 0] The length of s2: 5 The capacity of s2: 8 The value of s2: [0 0 0 0 0]
有一个窗口,你可以通过这个窗口看到一个数组,但是不一定能看到该数组中的所有元素,有时候只能看到连续的一部分元素。
切片代表的窗口也会被划分成一个一个的小格子,就像我们家里的窗户那样。每个小格子都对应着其底层数组中的某一个元素。
拿s2为例,这个窗口最左边的那个小格子对应的正好是其底层数组中的第一个元素,即索引为0的那个元素。因此可以说,s2中的索引从0到4所指向的元素恰恰就是其底层数组中索引从0到4代表的那 5 个元素。
当我们用make函数或切片值字面量(比如[]int{1, 2, 3})初始化一个切片时,该窗口最左边的那个小格子总是会对应其底层数组中的第 1 个元素。
但是当我们通过切片表达式基于某个数组或切片生成新切片的时候,情况就变得复杂起来了。
package main import "fmt" func main() { s3 := []int{1, 2, 3, 4, 5, 6, 7, 8} s4 := s3[3:6] fmt.Printf("The length of s4: %d ", len(s4)) fmt.Printf("The capacity of s4: %d ", cap(s4)) fmt.Printf("The value of s4: %d ", s4) }
输出:
The length of s4: 3 The capacity of s4: 5 The value of s4: [4 5 6]
这个s4的长度和容量分别是多少?
s4中的索引从0到2指向的元素对应的是s3及其底层数组中索引从3到5的那 3 个元素。
(切片与数组的关系)
切片的容量代表了它的底层数组的长度,但这仅限于使用make函数或者切片值字面量初始化切片的情况。
所以,s4的容量就是其底层数组的长度8, 减去上述切片表达式中的那个起始索引3,即5。
更通用的规则是:一个切片的容量可以被看作是透过这个窗口最多可以看到的底层数组中元素的个数。
由于s4是通过在s3上施加切片操作得来的,所以s3的底层数组就是s4的底层数组。又因为,在底层数组不变的情况下,切片代表的窗口可以向右扩展,直至其底层数组的末尾。
注意,切片代表的窗口是无法向左扩展的。也就是说,我们永远无法透过s4看到s3中最左边的那 3 个元素。
最后,顺便提一下把切片的窗口向右扩展到最大的方法。对于s4来说,切片表达式s4[0:cap(s4)]就可以做到。我想你应该能看懂。该表达式的结果值(即一个新的切片)会是[]int{4, 5, 6, 7, 8},其长度和容量都是5
package main import "fmt" func main() { s3 := []int{1, 2, 3, 4, 5, 6, 7, 8} s4 := s3[3:6] fmt.Printf("The length of s4: %d ", len(s4)) fmt.Printf("The capacity of s4: %d ", cap(s4)) fmt.Printf("The value of s4: %d ", s4) fmt.Println("The value of s4 extand: ", s4[0:cap(s4)]) }
输出:
The length of s4: 3 The capacity of s4: 5 The value of s4: [4 5 6] The value of s4 extand: [4 5 6 7 8]
一旦一个切片无法容纳更多的元素,Go 语言就会想办法扩容。但它并不会改变原来的切片,而是会生成一个容量更大的切片,然后将把原有的元素和新元素一并拷贝到新切片中。在一般的情况下,你可以简单地认为新切片的容量(以下简称新容量)将会是原切片容量(以下简称原容量)的 2 倍。但是,当原切片的长度(以下简称原长度)大于或等于1024时,Go 语言将会以原容量的1.25倍作为新容量的基准(以下新容量基准)。新容量基准会被调整(不断地与1.25相乘),直到结果不小于原长度与要追加的元素数量之和(以下简称新长度)。最终,新容量往往会比新长度大一些,当然,相等也是可能的。更多细节可参见runtime包中 slice.go 文件里的growslice及相关函数的具体实现
一个切片的底层数组永远不会被替换。为什么?虽然在扩容的时候 Go 语言一定会生成新的底层数组,但是它也同时生成了新的切片。它只是把新的切片作为了新底层数组的窗口,而没有对原切片,及其底层数组做任何改动。
在无需扩容时,append函数返回的是指向原底层数组的原切片,而在需要扩容时,append函数返回的是指向新底层数组的新切片。
只要新长度不会超过切片的原容量,那么使用append函数对其追加元素的时候就不会引起扩容。这只会使紧邻切片窗口右边的(底层数组中的)元素被新的元素替换掉。
切片是基于数组的,可变长的,并且非常轻快。一个切片的容量总是固定的,而且一个切片也只会与某一个底层数组绑定在一起。
切片的容量总会是在切片长度和底层数组长度之间的某一个值,并且还与切片窗口最左边对应的元素在底层数组中的位置有关系。
如果新的长度比原有切片的容量还要大,那么底层数组就一定会是新的,而且append函数也会返回一个新的切片。