Go基础学习05-数组和切片关系深度解析
切片和数组的联系
数组(array)和切片(slice)都属于集合类的类型,它们的值也都可以用来存储某一种类型的值(或者说元素)。数组和切片最重要的不同在于:
- 数组类型的值的长度是固定的,而切片类型的值的长度是可变长的。 数组的长度在声明它的时候就必须给定,并且在之后不会再改变。可以说,数组的长度是其类型的一部分。
- 切片的类型字面量中只有其元素的类型,而没有其长度。切片的长度可以自动地随着其中元素数量的增长而增长,但不会随着元素数量的减少而减少。
- 切片属于引用类型(切片、通道、函数),数组属于值类型(基础数据类型、结构体类型)。
数组和切片的关系:
把切片看作是对数组的一层简单的封装(在每个切片的底层数据结构中,一定包含一个数组);
数组可以看作切片的底层数据结构,切片也可以看作是对数组的某个连续片段的引用。
切片的容量和长度的关系:
示例代码1:通过make初始化切片
slice1 := make([]int, 5)
slice2 := make([]int, 5, 8)
fmt.Printf("slice1 len is : %d, cap is : %d\n", len(slice1), cap(slice1))
fmt.Printf("slice2 len is : %d, cap is : %d\n", len(slice2), cap(slice2))
上述代码运行结果:
slice1 len is : 5, cap is : 5
slice2 len is : 5, cap is : 8
当我们使用make函数初始化切片时,如果不指明那个量,那么切片的容量就会和长度一致。如果使用make函数初始化切片时,指明了切片的容量,那么切片的容量就是此时指明的数据,不一定等于切片的长度。通过make函数初始化的切片,其容量代表了切片的底层数组的长度,上述的5和8代表底层数组的长度,数组的长度不可改变。 既然切片底层数据结构数组的长度不可改变,那么当切片容量不足时需要扩容时如果扩?正确的答案:切片容量不足时,会开辟一块更大的空间(足够存储扩容后的所有元素),随后将切片原有元素复制到新的空间,并将新添加的元素追加到后面,随后再将新的引用传递给切片
示例代码2:使用切片表达式基于某个数组或切片生成新切片
slice3 := []int{1, 2, 3, 4, 5, 6, 7, 8}
slice4 := slice3[3:6]
fmt.Printf("slice3 elements is : %d\n", slice3)
fmt.Printf("slice4 elements is : %d\n", slice4)
fmt.Printf("slice3 len is : %d, cap is : %d\n", len(slice3), cap(slice3))
fmt.Printf("slice4 len is : %d, cap is : %d\n", len(slice4), cap(slice4))
上述代码执行结果:
slice3 elements is : [1 2 3 4 5 6 7 8]
slice4 elements is : [4 5 6]
slice3 len is : 8, cap is : 8
slice4 len is : 3, cap is : 5
提醒读者特意关注一下第四行的输出结果,可以查看一下是否符合预期。
slice4 := slice3[3 : 6]表示透过切片能看到底层数据结构数组的数据范围,左开右闭。所以表示的就是slice3中元素的索引范围从3到5(不包含6),所以slice4此时输出的元素是456。
此时对于slice4的长度计算就是结束索引 - 起始索引:6-3=3。所以len(slice4)就是3。为什么cap(slice4) = 5,而不是3呢?
使用切片表达式基于数组或者切片生成新的切片,其本质和原数组或者旧切片共用相同的底层数据结构,可以将新切片看作在旧切片的基础上的一个封装,一个拥有左右边界的窗口,左边界是切片表达式的左值,右边界是切片表达式的右值(右边界不能大于原有切片的最大容量,否则panic)。slice4的底层数组就是slice3的底层数组,又因为,在底层数组不变的情况下,切片代表的窗口可以向右扩展,直至其底层数组的末尾。所以,slice4的容量就是其底层数组的长度8减去上述切片表达式中的那个起始索引3,即5。
切片代表的窗口是无法向左扩展的,也就是说,我们永远无法透过slice4看到slice3中最左边的那三个元素
如何估算切片容量的增长
一旦一个切片无法容纳更多的元素,Go 语言就会想办法扩容。但它并不会改变原来的切片,而是会生成一个容量更大的切片,然后将把原有的元素和新元素一并拷贝到新切片中。
- 在一般的情况下,你可以简单地认为新切片的容量(以下简称新容量)将会是原切片容量(以下
简称原容量)的 2 倍。 - 但是,当原切片的长度(以下简称原长度)大于或等于1024时,Go 语言将会以原容量的1.25
倍作为新容量的基准(以下新容量基准)。新容量基准会被调整(不断地与1.25相乘),直到结果不小于原长度与要追加的元素数量之和(以下简称新长度)。最终,新容量往往会比新长度大一些,当然,相等也是可能的。 - 此外,如果我们一次追加的元素过多,以至于使新长度比原容量的 2 倍还要大,那么新容量就
会以新长度为基准。
关于切片底层数组替换的思考
一个切片的底层数组永远不会被替换。虽然在扩容的时候 Go 语言一定会生成新的底层数组,但是它也同时生成了新的切片。它是把新的切片作为了新底层数组的窗口,而没有对原切片及其底层数组做任何改动。
- 在无需扩容时,append函数返回的是指向原底层数组的新切片,而在需要扩容时,append函数返回的是指向新底层数组的新切片。
- 只要新长度不会超过切片的原容量,那么使用append函数对其追加元素的时候就不会引起扩容。这只会使紧邻切片窗口右边的(底层数组中的)元素被新的元素替换掉。
代码示例:
slice3 := []int{1, 2, 3, 4, 5, 6, 7, 8}
slice4 := slice3[3:7]
fmt.Printf("slice3 elements is : %d\n", slice3)
fmt.Printf("slice4 elements is : %d\n", slice4)
slice4 = append(slice4, 10)
fmt.Printf("slice3 elements is : %d\n", slice3)
fmt.Printf("slice4 elements is : %d\n", slice4)
运行结果:
slice3 elements is : [1 2 3 4 5 6 7 8]
slice4 elements is : [4 5 6 7]
slice3 elements is : [1 2 3 4 5 6 7 10]
slice4 elements is : [4 5 6 7 10]