数组和切片的相同点及不同点
共同点:
都属于集合类的类型,它们的值用来存储某一类型的值。
不同点:
-
数组类型的长度是固定的,它必须在声明它的时候就必须给定,并且之后不会再改变。可以说数组的长度是数组类型的一部分,例如[1]string和[2]string就是两个不同的数组类型。
-
切片类型的长度是可变长的,它的切片类型字面量中只有元素的类型而没有元素的长度。切片的长度可以自动随着其中元素数量的增长而增长,但不会随着元素数量的减少而减少。
-
数组和切片都有长度和容量的概念,分别可以通过内建函数
len()
和cap()
获取,其中,数组的容量永远 永远等于长度,都是不可变的。而切片的容量虽然相对复杂些但也是有规律可寻,后文后讲解如何估算一个切片的长度和容量。
本质上来说,我们可以把切片看做是对数组的一层简单的封装,每个切片的底层数据结构都是数组,它可以看作是对数组某个连续片段的引用,这里需要注意的几点是:
- Go语言中的切片类型属于引用类型,它是对数组某个连续片段的引用。同属引用类型的还有字典类型、通道类型和函数类型。而数组类型则属于值类型,同属于值类型的有基础数据类型和结构体类型
- 在Go语言中,判断函数所谓的传值还是传引用问题只需要看被传递的值的类型即可,若传递的值是值类型的,那么就是传值,反之若传递的值是引用类型的,那么就是引用传递。从传递成本的角度而言,引用类型的值要比值类型低得多
- 在数组和切片之上可以使用索引表达式访问某个具体的元素,也可以使用切片表达式得到一个新的切片
如何初始化一个切片
我们可以通过切片字面量表达式[]int{1,2,3}
和内建make函数make([]int,5,6)
初始化一个切片,也可以通过切片表达式基于某个数组或切片生成新切片,接下来分别描述下这几种场景:
通过切片表达式初始化切片
func main() {
s := []int{1, 2, 3, 4, 5, 6}
fmt.Printf("slice length: %d\n", len(s))
fmt.Printf("slice cap: %d\n", cap(s))
}
// 输出
// slice length: 6
// slice cap: 6
复制代码
通过这种方式初始化的切片其长度和容量都等于初始化时传入的元素数量。
通过make函数初始化切片
func main() {
s1 := make([]int, 5)
s2 := make([]int, 5, 6)
fmt.Printf("s1 length: %d\n", len(s1))
fmt.Printf("s1 cap: %d\n", cap(s1))
fmt.Printf("s2 length: %d\n", len(s2))
fmt.Printf("s2 cap: %d\n", cap(s2))
}
// 输出
// s1 length: 5
// s1 cap: 5
// s1 length: 5
// s1 cap: 6
复制代码
内建函数make接收三个参数,第一个参数为切片的类型字面量,第二个变量为切片的长度,第三个变量为切片的容量。当不指明切片容量的时,切片的容量就会和长度一致。
这里可以把切片看成一个窗口,通过这个窗口可以看到底层的数组,窗口被划分成一个一个的小格子,每个格子代表一个数组元素,但因为窗口大小有限因此不能看到数组中所有的元素,大部分时候只能看到数组连续的一部分元素。
当我们通过make函数或切片值字面量初始化的切片,它的第一个元素总是会对应其底层数组的第一个元素,在这种情况下,切片的容量就等于其底层数组的长度。拿s2为例,窗口最左边的格子对应的正好是其底层数组索引为0的第一个元素,因此,s2中索引从0到4的元素为其底层数组中索引从0到4代表的那5个元素。
基于某个数组或切片生成新切片
func main() {
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)
}
// 输出
// The length of s4:3
// The capacity of s4:5
// The value of s4:[4 5 6]
复制代码
在这里需要明白s3[3:6]
切片表达式中方括号两个整数其实就是数学中的区间表示法,其中3为起始索引,6为终止索引,代表s4从s3中索引为3的元素开始到索引为5的元素结束(不包含6),s4的长度就是6-3=3。因此我们可以说s4中的索引从0到2指向的元素对应的是s3中索引从3到5的那3个元素。
再来看看容量,一个切片的容量可以看做为通过这个窗口最多可以看到的底层数组重元素的个数。由于s4是在s3的基础上通过切片操作得来的,所以s4的底层数组就是s3的底层数组,因此s4可以向右扩展直至数组末尾,它的容量就是其底层数组的长度8减去s3的起始索引3,即5。
注意这里切片是无法向左扩展的,因此是永远无法透过s4的窗口看到s3的前三个元素的。
若要将s4的窗口向右扩展到最大,可以通过切片表达式s4[0,cap(s4)]做到,它的结果值为[]int{4,5,6,7,8}
切片容量的增长规律
func main() {
s5 := []int{1, 2, 3, 4, 5, 6}
s6 := s5[0:2]
s6 = append(s6, 66)
fmt.Printf("The value of s6:%d\n", s6)
fmt.Printf("The value of s5:%d\n", s5)
}
// 输出
// The value of s6:[1 2 66]
// The value of s5:[1 2 66 4 5 6]
复制代码
可以通过append函数对切片进行扩展(这里append函数返回的是一个新切片,因此需要用切片类型的变量去接收它的返回值),只要新长度不会超过切片的原容量时,那么使用append函数对其追加元素时就不会引起扩容,只会使得紧邻切片窗口右边的(底层数组中的)元素被新的元素替换掉,生成一个指向原先底层数组的新切片。
当新长度超过切片的容量时,append函数返回的是指向新底层数组的新切片,它会生成一个新的底层数组,然后将原有的元素和新的元素拷贝到新数组中,然后再生成一个新切片指向这个新的底层数组,在一般情况下,可以简单地认为新切片的容量时原切片容量的两倍。