数组与切片
数组与切片的关系
数组与切片都是属于集合类的类型,并且它们的值也都可以用来存储某一种类型的值(或者说元素)。
数组与切片最重要的不同点在于: 数组类型的值(以下简称数组)的长度是固定的,而切片类型的值(以下简称切片)****是可变长的。
数组可以被叫做切片的底层数组,而切片也可以被看作是对数组的某个连续片段的引用。
数组与切片的声明
数组的长度在声明它的时候就必须给定,并且之后不会再改变。可以说,数组的长度是其类型的一部分。比如,[1]string和[2]string就是两个不同的数组类型。
var identifier [len]type
myArray := [3]int{1,2,3}
而切片的类型字面量中只有元素的类型,而没有长度。切片的长度可以自动地随着其中元素 数量的增长而增长,但不会随着元素数量的减少而减小,比如 []string。
var identifier []type
mySlice1 := []int#直接声明
mySlice2 := new([]int)#New 返回指针地址
mySlice3 := make([]int, 10, 20)#Make 返回第一个元素,可预设内存空间,避免未来的内存拷贝
Go中的引用类型和值类型
切片可以看成是对数组的某个片段的引用,所以切片类型属于引用类型,同属引用类型的还有字典类型、通道类型、函数类型等;而 Go 语言的数组类型则属于值类型,同属值类型的有基础数据类型以及结构体类型。
Go中的值传递和引用传递
Go 语言里不存在像 Java 等编程语言中令人困惑的“传值或传引用”问题,我们只需要关注被传递值的类型就好:
如果传递的值是引用类型的,那么就是“传引用”。如果传递的值是值类型的,那么就是“传值”。从传递成本的角度讲,引用类型的值往往要比值类型的值低很多。
索引表达式与切片表达式
我们在数组和切片之上都可以应用索引表达式,得到的都会是某个元素。我们在它们之上也都可以应用切片表达式,也都会得到一个新的切片。
切片的长度和容量
在Go中我们可以通过调用内建函数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)
}
问题分析 - 切片s1和s2的容量都是多少?
切片s1和s2的容量分别是5和8:
- s1分析:我们使用make声明s1的时候把它的长度设置成了5。当我们用make函数初始化切片时,如果不指明其容量,那么它就会和长度一致,所以s1的容量是5.
- s2分析: 如果在初始化时指明了容量,那么切片的实际容量也就是它了,所以s2的容量是8
事实上,切片的容量可以看成是底层数组的长度,而切片可以看成是一个数组之上的窗口,切片的长度就是窗口的长度,我们可以通过这个窗口看到一个数组,但是不一定能看到该数组中的所有元素,有时候只能看到连续的一部分元素。
比如:
s2的长度是5,所以你可以看到底层数组中的第 1 个元素到第 5 个元素,对应的底层数组的索引范围是 [0, 4]。
窗口首元素mapping
切片代表的窗口也会被划分成一个一个的小格子**,**每个小格子都对应着其底层数组中的某一个元素。
当我们用make函数或切片值字面量(比如[]int{1, 2, 3})初始化一个切片时,该窗口最左边的那个小格子总是会对应其底层数组中的第 1 个元素,但是当我们通过切片表达式基于某个数组或切片生成新切片的时候,情况就变得复杂起来了。
代码示例
s3 := []int{1, 2, 3, 4, 5, 6, 7, 8} //s3中有 8 个元素,分别是从1到8的整数。s3的长度和容量都是8
s4 := s3[3:6]//使用切片表达式s3[3:6]初始化了切片s4,长度为6 -3 =3,[3:6),容量为8 - 3 = 5
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)
通用规则:
- 一个切片的容量可以被看作是透过这个窗口最多可以看到的底层数组中元素的个数。
- 在底层数组不变的情况下,切片代表的窗口只可以向右扩展,直至其底层数组的末尾。
如何将窗口拓展到最大?
s4[0:cap(s4)] -> []int{4, 5, 6, 7, 8}
切片的扩容规则
-
一旦一个切片无法容纳更多的元素,Go 语言就会想办法扩容。但它并不会改变原来的切片,而是会生成一个容量更大的切片,然后将把原有的元素和新元素一并拷贝到新切片中。在一般的情况下,你可以简单地认为新切片的容量(以下简称新容量)将会是原切片容量(以下简称原容量)的 2 倍。
-
当原切片的长度(以下简称原长度)大于或等于1024时,Go 语言将会以原容量的1.25倍作为新容量的基准(以下新容量基准)。新容量基准会被调整(不断地与1.25相乘),直到结果不小于原长度与要追加的元素数量之和(以下简称新长度)。最终,新容量往往会比新长度大一些,当然,相等也是可能的。
-
如果我们一次追加的元素过多,以至于使新长度比原容量的 2 倍还要大,那么新容量就会以新长度为基准。
切片的底层数组什么时候会被替换?
实际上一个切片的底层数组永远不会被替换,因为在扩容的时候 Go 语言会生成新的底层数组,但是它也同时生成了新的切片,它只是把新的切片作为了新底层数组的窗口,而没有对原切片,及其底层数组做任何改动。
Tips: 在无需扩容时,append函数返回的是指向原底层数组的新切片,而在需要扩容时,append函数返回的是指向新底层数组的新切片。