这篇文章主要介绍关于Go语言切片的一些知识

回顾

数组和切片最主要的区别在于数组的长度是固定的, 而切片的长度是可以动态改变的, 在Go语言中使用make()函数来创建一个切片值

1
2
3
4
5
slice := make([]string, 4, 4)

if slice == nil {
fmt.Println("slice is nil\n")
}

使用make()函数创建切片的时候, 第一个参数是类型字面量, 第二个参数是长度, 第三个参数是容量, 如果第三个参数没有写的话那么将默认和长度相同, 我们可以使用内建的函数len()cap()来查看切片的长度和容量。

切片的底层实现

如果你看过Go语言slice的底层实现的话, 应该会知道slice其实是一个结构体, 下面是slice的底层实现:

1
2
3
4
5
type slice struct {
array unsafe.Pointer
len int
cap int
}

熟悉C/C++的人都知道, 在结构体中使用数组指针的问题–数据会发生共享, 相当于我们封装了指针, 自已构造出一种引用类型。在Go语言/C语言中我们是可以感受到值类型的存在的,

  • array是一个指针, 指向切片的底层数组, 这个指针不一定会指在数组的头部分, 也可能会指向数组的腰子
  • len表示的是可以从底层数组取到元素的区间的长度, 如果没有写初始位置, 区间的初始位置就是0, 这个在后面会提到
  • cap表示底层数组的长度, 但是这种情况仅限与make函数或切片值字面量初始化切片的情况

所以我们就会发现其实lencap其实是起到一种限制array指针遍历范围的作用。

当我们通过切片表达式基于某个数组或切片生成一个新的切片的时候, 情况就会变得有点复杂

1
2
3
4
5
6
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)

切片s3中有8个元素, 所以s3的容量和长度都为8, 然后我们基于s3生成了一个切片s4, 这样这两个切片就基于了同一个底层数组.

那么问题来了切片s4的长度是多少?

想搞清楚这个问题, 我们需要知道[3:6]这个表达式的含义, 这个表达式表示的是一种取值范围, 就像数学中的区间一样, 它表示的是能从底层数组取到的元素的区间, 同时需要注意的是这个区间是左开右闭的。上面的[3:6]实际上是[3:6)。长度(len)就是这个区间的范围, 所以切片s4的长度为3

说完长度, 我们来说容量: 上面我们也定义了容量的概念, 但是这个定义并不是很通用, 更加通用的说法是: 一个切片的容量是透过底层数组最多可以看到的元素的个数。记住是看到, 不是能取到, 这是两个完全不同的概念。所以s4的容量是5.

切片的扩容机制

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

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

append函数的操作

不需要扩容时候, append函数返回的是指向原底层数组的原切片,而在需要扩容时,append函数返回的是指向新底层数组的新切片。

于是出现下面的这种:

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 newSlice(slice2 []int) {
//在这里你会发现切片会扩容
slice2 = append(slice2, 2)
fmt.Println(slice2[2])
}

func main() {
slice1 := make([]int, 2)
slice1[0] = 0;
slice1[1] = 1;

newSlice(slice1)

//你在这里访问会直接报panic
fmt.Println(slice1[2])
}

上面的例子其实和下面是对应的:

1
2
3
4
5
6
7
8
a := make([]int, 32)
b := a[1:16]

//将a这引用指向别的地方
a = append(a, 1)
a[2] = 42

fmt.Println(b[2]) //输出0