数组类型的值(如下简称数组)的长度是固定的,而切片类型的值(如下简称切片)是可变长的。编程
数组的长度在声明它的时候就必须给定,而且以后不会再改变。能够说,数组的长度是其类型的一部分。好比,[1]string和[2]string就是两个不一样的数组类型。而切片的类型字面量中只有元素的类型,而没有长度。切片的长度能够自动地随着其中元素数量的增加而增加,但不会随着元素数量的减小而减少。数组
其实能够把切片看作是对数组的一层简单的封装,由于在每一个切片的底层数据结构中,必定会包含一个数组。数组能够被叫作切片的底层数组,而切片也能够被看做是对数组的某个连续片断的引用。数据结构
Go 语言的切片类型属于引用类型,同属引用类型的还有字典类型、通道类型、函数类型等;而 Go 语言的数组类型则属于值类型,同属值类型的有基础数据类型以及结构体类型。app
Go 语言里不存在像 Java 等编程语言中使人困惑的“传值或传引用”问题。在 Go 语言中,咱们判断所谓的“传值”或者“传引用”只要看被传递的值的类型就行了。若是传递的值是引用类型的,那么就是“传引用”。若是传递的值是值类型的,那么就是“传值”。从传递成本的角度讲,引用类型的值每每要比值类型的值低不少。编程语言
咱们在数组和切片之上均可以应用索引表达式,获得的都会是某个元素。咱们在它们之上也均可以应用切片表达式,也都会获得一个新的切片。ide
调用内建函数len,获得数组和切片的长度。经过调用内建函数cap,咱们能够获得它们的容量。但要注意,数组的容量永远等于其长度,都是不可变的。切片的容量却不是这样,而且它的变化是有规律可寻的。函数
package main import "fmt" func main() { // 示例1。 s1 := make([]int, 5) fmt.Printf("The length of s1: %d\n", len(s1)) fmt.Printf("The capacity of s1: %d\n", cap(s1)) fmt.Printf("The value of s1: %d\n", s1) s2 := make([]int, 5, 8) fmt.Printf("The length of s2: %d\n", len(s2)) fmt.Printf("The capacity of s2: %d\n", cap(s2)) fmt.Printf("The value of s2: %d\n", s2) fmt.Println()
go run demo15.go 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]
我用内建函数make声明了一个[]int类型的变量s1。我传给make函数的第二个参数是5,从而指明了该切片的长度。我用几乎一样的方式声明了切片s2,只不过多传入了一个参数8以指明该切片的容量。如今,具体的问题是:切片s1和s2的容量都是多少?学习
这道题的典型回答:切片s1和s2的容量分别是5和8。
s1的容量为何是5呢?由于我在声明s1的时候把它的长度设置成了5。当咱们用make函数初始化切片时,若是不指明其容量,那么它就会和长度一致。若是在初始化时指明了容量,那么切片的实际容量也就是它了。这也正是s2的容量是8的缘由。code
过s2再来明确下长度、容量以及它们的关系。我在初始化s2表明的切片时,同时也指定了它的长度和容量。我在刚才说过,能够把切片看作是对数组的一层简单的封装,由于在每一个切片的底层数据结构中,必定会包含一个数组。数组能够被叫作切片的底层数组,而切片也能够被看做是对数组的某个连续片断的引用。在这种状况下,切片的容量实际上表明了它的底层数组的长度,这里是8。(注意,切片的底层数组等同于咱们前面讲到的数组,其长度不可变。)blog
有一个窗口,你能够经过这个窗口看到一个数组,可是不必定能看到该数组中的全部元素,有时候只能看到连续的一部分元素。
这个数组就是切片s2的底层数组,而这个窗口就是切片s2自己。s2的长度实际上指明的就是这个窗口的宽度,决定了你透过s2,能够看到其底层数组中的哪几个连续的元素。因为s2的长度是5,因此你能够看到底层数组中的第 1 个元素到第 5 个元素,对应的底层数组的索引范围是 [0, 4]。切片表明的窗口也会被划分红一个一个的小格子,就像咱们家里的窗户那样。每一个小格子都对应着其底层数组中的某一个元素。
s2为例,这个窗口最左边的那个小格子对应的正好是其底层数组中的第一个元素,即索引为0的那个元素。所以能够说,s2中的索引从0到4所指向的元素偏偏就是其底层数组中索引从0到4表明的那 5 个元素。
咱们用make函数或切片值字面量(好比[]int{1, 2, 3})初始化一个切片时,该窗口最左边的那个小格子老是会对应其底层数组中的第 1 个元素。
package main import "fmt" func main() { // 示例2。 s3 := []int{1, 2, 3, 4, 5, 6, 7, 8} s4 := s3[3:6] fmt.Printf("The length of s4: %d\n", len(s4)) fmt.Printf("The capacity of s4: %d\n", cap(s4)) fmt.Printf("The value of s4: %d\n", s4) fmt.Println()
go run demo15.go The length of s4: 3 The capacity of s4: 5 The value of s4: [4 5 6]
切片s3中有 8 个元素,分别是从1到8的整数。s3的长度和容量都是8。而后,我用切片表达式s3[3:6]初始化了切片s4。问题是,这个s4的长度和容量分别是多少?
切片表达式中的方括号里的那两个整数[3:6]都表明什么?
[3:6]要表达的就是透过新窗口能看到的s3中元素的索引范围是从3到5(注意,不包括6)。这里的3可被称为起始索引,6可被称为结束索引。那么s4的长度就是6减去3,即3。所以能够说,s4中的索引从0到2指向的元素对应的是s3及其底层数组中索引从3到5的那 3 个元素。
再来看容量。我在前面说过,切片的容量表明了它的底层数组的长度,但这仅限于使用make函数或者切片值字面量初始化切片的状况。
更通用的规则是:一个切片的容量能够被看做是透过这个窗口最多能够看到的底层数组中元素的个数。
因为s4是经过在s3上施加切片操做得来的,因此s3的底层数组就是s4的底层数组。又由于,在底层数组不变的状况下,切片表明的窗口能够向右扩展,直至其底层数组的末尾。因此,s4的容量就是其底层数组的长度8, 减去上述切片表达式中的那个起始索引3,即5。切片表明的窗口是没法向左扩展的。也就是说,咱们永远没法透过s4看到s3中最左边的那 3 个元素。
顺便提一下把切片的窗口向右扩展到最大的方法。对于s4来讲,切片表达式s4[0:cap(s4)]就能够作到。我想你应该能看懂。该表达式的结果值(即一个新的切片)会是[]int{4, 5, 6, 7, 8},其长度和容量都是5。
一旦一个切片没法容纳更多的元素,Go 语言就会想办法扩容。但它并不会改变原来的切片,而是会生成一个容量更大的切片,而后将把原有的元素和新元素一并拷贝到新切片中。在通常的状况下,你能够简单地认为新切片的容量(如下简称新容量)将会是原切片容量(如下简称原容量)的 2 倍。
可是,当原切片的长度(如下简称原长度)大于或等于1024时,Go 语言将会以原容量的1.25倍做为新容量的基准(如下新容量基准)。新容量基准会被调整(不断地与1.25相乘),直到结果不小于原长度与要追加的元素数量之和(如下简称新长度)。最终,新容量每每会比新长度大一些,固然,相等也是可能的。
另外,若是咱们一次追加的元素过多,以致于使新长度比原容量的 2 倍还要大,那么新容量就会以新长度为基准。注意,与前面那种状况同样,最终的新容量在不少时候都要比新容量基准更大一些。更多细节可参见runtime包中 slice.go 文件里的growslice及相关函数的具体实现。
package main import "fmt" func main() { // 示例1。 s6 := make([]int, 0) fmt.Printf("The capacity of s6: %d\n", cap(s6)) for i := 1; i <= 5; i++ { s6 = append(s6, i) fmt.Printf("s6(%d): len: %d, cap: %d\n", i, len(s6), cap(s6)) } fmt.Println() // 示例2。 s7 := make([]int, 1024) fmt.Printf("The capacity of s7: %d\n", cap(s7)) s7e1 := append(s7, make([]int, 200)...) fmt.Printf("s7e1: len: %d, cap: %d\n", len(s7e1), cap(s7e1)) s7e2 := append(s7, make([]int, 400)...) fmt.Printf("s7e2: len: %d, cap: %d\n", len(s7e2), cap(s7e2)) s7e3 := append(s7, make([]int, 600)...) fmt.Printf("s7e3: len: %d, cap: %d\n", len(s7e3), cap(s7e3)) fmt.Println() // 示例3。 s8 := make([]int, 10) fmt.Printf("The capacity of s8: %d\n", cap(s8)) s8a := append(s8, make([]int, 11)...) fmt.Printf("s8a: len: %d, cap: %d\n", len(s8a), cap(s8a)) s8b := append(s8a, make([]int, 23)...) fmt.Printf("s8b: len: %d, cap: %d\n", len(s8b), cap(s8b)) s8c := append(s8b, make([]int, 45)...) fmt.Printf("s8c: len: %d, cap: %d\n", len(s8c), cap(s8c)) }
go run demo16.go The capacity of s6: 0 s6(1): len: 1, cap: 1 s6(2): len: 2, cap: 2 s6(3): len: 3, cap: 4 s6(4): len: 4, cap: 4 s6(5): len: 5, cap: 8 The capacity of s7: 1024 s7e1: len: 1224, cap: 1280 s7e2: len: 1424, cap: 1696 s7e3: len: 1624, cap: 2048 The capacity of s8: 10 s8a: len: 21, cap: 22 s8b: len: 44, cap: 44 s8c: len: 89, cap: 96
切地说,一个切片的底层数组永远不会被替换。为何?虽然在扩容的时候 Go 语言必定会生成新的底层数组,可是它也同时生成了新的切片。
它只是把新的切片做为了新底层数组的窗口,而没有对原切片,及其底层数组作任何改动。
请记住,在无需扩容时,append函数返回的是指向原底层数组的新切片,而在须要扩容时,append函数返回的是指向新底层数组的新切片。因此,严格来说,“扩容”这个词用在这里虽然形象但并不合适。不过鉴于这种称呼已经用得很普遍了,咱们也不必另找新词了。
顺便说一下,只要新长度不会超过切片的原容量,那么使用append函数对其追加元素的时候就不会引发扩容。这只会使紧邻切片窗口右边的(底层数组中的)元素被新的元素替换掉。你能够运行 demo17.go 文件以加强对这些知识的理解。