Notes
Go
切片

切片(Slice)

正如上面数组提到的,数组是固定长度的,无法改变大小。但是这无法满足大多数的需求,所以 Go 语言中提供了切片。切片可以看作大小可变的数组

切片的原理

切片在底层封装了一个数组指针,打开 slice.go 文件可以看到:

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}
  • array:指向数组的指针
  • len:切片的长度,即切片中元素的个数
  • cap:切片的容量,本质上是底层数组的长度

切片的内部结构的示意图如下:

     +-------+-------+-------+
Slice| array |  len  |  cap  |
     +-------+-------+-------+
          |
          v
     +-------+-------+-------+-------+-------+-------+
Array|   1   |   2   |   3   |   4   |   5   |   6   |
     +-------+-------+-------+-------+-------+-------+

切片的声明和定义

切片的声明和定义格式为:

var 切片名 []切片类型

与数组的声明类似,但是不需要指定切片的长度。

可以调用 make 函数来创建切片:

s:= make([]int, 5, 10)
 
fmt.Println(len(s)) // 5
fmt.Println(cap(s)) // 10
👾

make 函数适用于创建对象的通用方法,例如创建切片、字典、通道等。在这里第一个参数是类型,第二个参数是长度,第三个参数是容量。

切片的访问

切片的访问方式与数组类似,通过索引来访问。

s:= make([]int, 5, 10)
 
s[0] = 1
s[1] = 2
 
for i:= 0; i < len(s); i++ {
  fmt.Println(s[i])
}

执行结果:

1
2
0
0
0

这表示,未经初始化的切片的值为当且类型的零值

如果使用 for-range 循环,可以更加简洁地访问切片:

s:= make([]int, 5, 10)
 
s[0] = 1
s[1] = 2
 
for _, v:= range s {
  fmt.Println(v)
}

for-range 循环是由切片的长度而非容量决定的。

拓展切片长度

我们能否通过索引来拓展切片的长度呢?

s:= make([]int, 5, 10)
 
for i:= 0; i < 10; i++ {
  s[i] = i
}
 
fmt.Println(s)

执行结果:

panic: runtime error: index out of range [5] with length 5

不行。Go 抛出了错误提示,表示索引超出了切片的长度(越界)。

正确的做法是使用 append 函数:

s:= make([]int, 5, 10)
 
for i:= 0; i < len(s); i++ {
  s[i] = i
}
 
s = append(s, 5, 6, 7, 8, 9)

append 函数第一个参数为切片,后面的参数为要追加的元素。追加后的切片长度为原始切片长度加上追加的元素的个数。使用 append 函数时,如果将返回值传递给原始切片,那么原始切片将被替换,但如果传递给其他变量,那么原始切片将不会被替换。

s:= make([]int, 5, 10)
 
for i:= 0; i < len(s); i++ {
  s[i] = i
}
 
a:= append(s, 5, 6, 7, 8, 9)
 
fmt.Println(s) // [0 1 2 3 4]
fmt.Println(a) // [0 1 2 3 4 5 6 7 8 9]

拓展切片容量

切片的容量是底层数组的长度,当新的元素超出容量时,Go 会重建底层的数组。

s:= make([]int, 5, 10)
 
for i:= 0; i < len(s); i++ {
  s[i] = i
}
 
fmt.Println(len(s)) // 5
 
s = append(s, 5, 6, 7, 8, 9)
 
fmt.Println(len(s)) // 10
 
fmt.Println(cap(s)) // 10
 
// 打印指针
fmt.Printf("%p\n", s) // 0xc0000b6010
 
// 继续追加
s = append(s, 10)
 
fmt.Println(len(s)) // 11
 
fmt.Println(cap(s)) // 20
 
// 打印指针
fmt.Printf("%p\n", s) // 0xc0000b60b0

可以观察得到,当切片的容量不足时,Go 会重新分配底层数组,并将原始切片的值复制到新的切片中。这也导致了切片的指针发生了变化。

利用数组创建切片

a:= [5]int{1, 2, 3, 4, 5}
 
s:= a[1:3]
 
m:= a[:3]
 
n:= a[1:]
 
k:= a[:]
 
fmt.Println(s) // [2 3]
fmt.Println(m) // [1 2 3]
fmt.Println(n) // [2 3 4 5]
fmt.Println(k) // [1 2 3 4 5]

array[m:n] 表示从数组的第 m 个元素到第 n-1 个元素,array[:n] 表示从数组的第一个元素到第 n-1 个元素,array[m:] 表示从数组的第 m 个元素到最后一个元素,array[:] 表示整个数组。

这和 Python 中的切片操作类似。

进一步观察底层,让我们打印数组 a、切片 s、切片 m、切片 n、切片 k 的指针:

fmt.Printf("%p\n", a) // 0xc0000b6010
fmt.Printf("%p\n", s) // 0xc0000b6018
fmt.Printf("%p\n", m) // 0xc0000b6010
fmt.Printf("%p\n", n) // 0xc0000b6018
fmt.Printf("%p\n", k) // 0xc0000b6010

可以看出切片 s 的底层数组比原数组地址增长了 8 个字节,而切片 m 和切片 k 的底层数组地址和原数组地址相同,这是因为切片 m 和切片 k 的起始索引为 0。之所以是 8 个字节是因为运行环境是 64 位的,一个 int 类型占用 8 个字节。

利用切片创建切片

s:= make([]int, 5, 10)
 
for i:= 0; i < len(s); i++ {
  s[i] = i
}
 
m:= s[1:3]
 
n:= s[:3]
 
fmt.Println(m) // [1 2]
fmt.Println(n) // [0 1 2]

同样地,切片 m 和切片 n 的底层数组地址和原切片地址不同,切片 m 的底层数组地址比原切片地址增长了 8 个字节,而切片 n 的底层数组地址和原切片地址相同。

切片元素的修改

由于可能多个切片共享同一个底层数组,所以修改一个切片的元素可能会影响到其他切片。

s := []int{1, 2, 3, 4, 5}
 
m := s[1:3]
 
m[0] = 6
 
fmt.Println(s) // [1 6 3 4 5]
fmt.Println(m) // [6 3]