切片(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]