GO:2.1 数组和切片

1、数组(array)类型和切片(slice)类型的异同

1、相同

数组(array)类型和切片(slice)类型的共同点是都属于集合类的类型,并且,它们的值也都可以用来存储某一种类型的值(或者说元素)。

2、区别

它们最重要的不同是:数组类型的值(以下简称数组)的长度是固定的,而切片类型的值(以下简称切片)是可变长的。

数组的长度在声明它的时候就必须给定,并且之后不会再改变。可以说,数组的长度是其类型的一部分。比如,[1]string和[2]string就是两个不同的数组类型

切片的类型字面量中只有元素的类型,而没有长度。切片的长度可以自动地随着其中元素数量的增长而增长,但不会随着元素数量的减少而减小。
arrslice1

我们其实可以把切片看做是对数组的一层简单的封装,因为在每个切片的底层数据结构中,一定会包含一个数组。数组可以被叫做切片的底层数组,而切片也可以被看作是对数组的某个连续片段的引用。

  • 也正因为如此,Go 语言的切片类型属于引用类型,同属引用类型的还有字典类型、通道类型、函数类型等;而 Go 语言的数组类型则属于值类型,同属值类型的有基础数据类型以及结构体类型。注意,Go 语言里不存在像 Java 等编程语言中令人困惑的“传值或传引用”问题。在 Go 语言中,我们判断所谓的“传值”或者“传引用”只要看被传递的值的类型就好了。如果传递的值是引用类型的,那么就是“传引用”。如果传递的值是值类型的,那么就是“传值”。从传递成本的角度讲,引用类型的值往往要比值类型的值低很多。我们在数组和切片之上都可以应用索引表达式,得到的都会是某个元素。我们在它们之上也都可以应用切片表达式,也都会得到一个新的切片

  • 通过调用内建函数len,得到数组和切片的长度。通过调用内建函数cap,我们可以得到它们的容量。但要注意,数组的容量永远等于其长度,都是不可变的。切片的容量却不是这样,并且它的变化是有规律可寻的。

2、怎样正确估算切片的长度和容量?

1、如下例子demo15.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

func main() {
// 示例1。
s1 := make([]int, 5)
//用内建函数make声明了一个[]int类型的变量s1,传给make函数的第二个参数是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)
//同样的方式声明了切片s2,多传入了一个参数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的容量是5,s2的容量是8

2、通过切片表达式基于某个数组或切片生成新切片的时候,情况就变得复杂起来了。
如下例子:

1
2
3
4
5
6
7
8
9
s3 := []int{1, 2, 3, 4, 5, 6, 7, 8}
//s3中有 8 个元素,分别是从1到8的整数,s3的长度和容量都是8。

s4 := s3[3:6]
//用切片表达式s3[3:6]初始化了切片s4

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)

运行

1
2
3
The length of s4: 3
The capacity of s4: 5
The value of s4: [4 5 6]

  • 1、s4的长度是多少?
    长度是3

切片表达式中s3[3:6]取数范围为[3,6),即从索引3开始取,取到索引6但不包含索引6。

这里的3可被称为起始索引,6可被称为结束索引。那么s4的长度就是6减去3,即3。

可以说,s4中的索引从0到2指向的元素对应的是s3及其底层数组中索引从3到5的那 3 个元素。
arrslice2

  • 2、再来看s4的容量是多少?
    容量是5

    切片的容量代表了它的底层数组的长度,但这仅限于使用make函数或者切片值字面量初始化切片的情况。

更通用的规则是:一个切片的容量可以被看作是透过这个窗口最多可以看到的底层数组中元素的个数。

由于s4是通过在s3上施加切片操作得来的,所以s3的底层数组就是s4的底层数组。

又因为,在底层数组不变的情况下,切片代表的窗口可以向右扩展,直至其底层数组的末尾。

所以,s4的容量就是其底层数组的长度8, 减去上述切片表达式中的那个起始索引3,即5。

注意,切片代表的窗口是无法向左扩展的。也就是说,我们永远无法透过s4看到s3中最左边的那 3 个元素。

  • 把切片的窗口向右扩展到最大的方法。
    对于s4来说,切片表达式s4[0:cap(s4)]就可以做到。我想你应该能看懂。该表达式的结果值(即一个新的切片)会是[]int{4, 5, 6, 7, 8},其长度和容量都是5。

3、怎样估算切片容量的增长?

一旦一个切片无法容纳更多的元素,Go 语言就会想办法扩容。但它并不会改变原来的切片,而是会生成一个容量更大的切片,然后将把原有的元素和新元素一并拷贝到新切片中。在一般的情况下,你可以简单地认为新切片的容量(以下简称新容量)将会是原切片容量(以下简称原容量)的 2 倍。

但是,当原切片的长度(以下简称原长度)大于或等于1024时,Go 语言将会以原容量的1.25倍作为新容量的基准(以下新容量基准)新容量基准会被调整(不断地与1.25相乘),直到结果不小于原长度与要追加的元素数量之和(以下简称新长度)。最终,新容量往往会比新长度大一些,当然,相等也是可能的。

另外,如果我们一次追加的元素过多,以至于使新长度比原容量的 2 倍还要大,那么新容量就会以新长度为基准。注意,与前面那种情况一样,最终的新容量在很多时候都要比新容量基准更大一些。

4、切片的底层数组什么时候会被替换?

确切地说,一个切片的底层数组永远不会被替换。为什么?虽然在扩容的时候 Go 语言一定会生成新的底层数组,但是它也同时生成了新的切片。

它只是把新的切片作为了新底层数组的窗口,而没有对原切片,及其底层数组做任何改动。

在无需扩容时,append函数返回的是指向原底层数组的新切片,而在需要扩容时,append函数返回的是指向新底层数组的新切片。所以,严格来讲,“扩容”这个词用在这里虽然形象但并不合适。

只要新长度不会超过切片的原容量,那么使用append函数对其追加元素的时候就不会引起扩容。这只会使紧邻切片窗口右边的(底层数组中的)元素被新的元素替换掉