slice 是 Go 里非常强大的复合结构,简单的说,它就是一个动态数组。
1. slice 声明与定义
不同于数组的声明,slice 声明是不需要指定长度的。比如下面这样:
var a [5]int // 这是数组
var b []int // 这是 slice
这样一来,数组与 slice 的区别就非常明显了。
下面是常见的声明和定义方式。
- 例 1
package main
import "fmt"
func main() {
var a []int
var b []int = []int{1, 2, 3}
var c = []string{"Allen", "Luffy"}
d := []float64{3.4, 5.6, 0.9}
e := make([]int, 10) // 使用 make 函数创建并返回一个大小为 10 的 slice
var f [5]int = [5]int{4, 5, 3} // 好像混入了点什么?^_^
fmt.Printf("a = %v, type = %[1]T, len = %d, cap = %d\n", a, len(a), cap(a))
fmt.Printf("b = %v, type = %[1]T, len = %d, cap = %d\n", b, len(b), cap(b))
fmt.Printf("c = %v, type = %[1]T, len = %d, cap = %d\n", c, len(c), cap(c))
fmt.Printf("d = %v, type = %[1]T, len = %d, cap = %d\n", d, len(d), cap(d))
fmt.Printf("e = %v, type = %[1]T, len = %d, cap = %d\n", e, len(e), cap(e))
fmt.Printf("f = %v, type = %[1]T, len = %d, cap = %d\n", f, len(f), cap(f))
}
输出为:
a = [], type = []int, len = 0, cap = 0
b = [1 2 3], type = []int, len = 3, cap = 3
c = [Allen Luffy], type = []string, len = 2, cap = 2
d = [3.4 5.6 0.9], type = []float64, len = 3, cap = 3
e = [0 0 0 0 0 0 0 0 0 0], type = []int, len = 10, cap = 10
f = [4 5 3 0 0], type = [5]int, len = 5, cap = 5
上面的例子除了 f 是一个 [5]int
数组外,其它的都是 slice. 函数 len 我们之前也见过了,它可以用来计算 string 的长度,在这里,也可以用来计算 array 和 slice 的长度。而 cap 函数,是我们第一次遇见,它表示数组或 slice 底层真正的容量大小。
2. 如何给 slice 追加元素?
先来看下面的例子。
- 例 2
package main
import "fmt"
func main() {
var a []int = []int{1, 2, 3}
fmt.Printf("a = %v\n", a)
a[3] = 1
fmt.Printf("a = %v\n", a)
}
这段代码可以编译通过,但是运行时出错了:
图1 运行时异常
可以看到,第一个 Printf
正常输出了数组 a,执行到 a[3] = 1
时程序运行时异常了。原因是索引越界了。
不是说 slice 是动态数组吗,为什么添加个元素就不行了呢?在第 1 小节我们其实已经用过了 cap 函数了,它是用来计算 slice 底层真正的容量的。在这里 a 的 cap 容量刚好是 3,因此使用 a[3] = 1
一定是不能成功的。
Go 提供了一个内建函数 append
可以向 slice 进行追加元素,如果 slice 容量不够,它会自动的扩容。我们再修改一个版本,如例 3.
- 例 3
package main
import "fmt"
func main() {
var a []int = []int{1, 2, 3}
fmt.Printf("a = %v\n", a)
_ = append(a, 1) // append 会返回一个新的 slice,暂时忽略
fmt.Printf("a = %v\n", a)
}
图2 追加元素失败?
从输出结果上看,好像并不正确?注意例 3 中的注释『append 会返回一个新的 slice』,这样吧,我们把返回的新 slice 的结果也打印出来看看。见例 4.
- 例 4
package main
import "fmt"
func main() {
var a []int = []int{1, 2, 3}
fmt.Printf("a = %v\n", a)
b := append(a, 1)
fmt.Printf("a = %v\n", a)
fmt.Printf("b = %v\n", b)
}
图3 slice b 才是那个被 append 的那个数组
这样你应该就能明白了,append 并不是真的把 a 给扩容了,它只是申请了一个容量更大的新 slice,然后把 a 里的元素复制到了新 slice,再追加一个元素。
append 的策略伪代码大概如下:
append(a []T, e T):
l = len(a)
c = cap(a)
if l + 1 < c:
a[l+1] = e
return a
else:
b = make([]T, 2*c) // 申请一个以前容量 2 倍的 slice
copy(b, a) // 把 a 中的元素复制到 b
b[l + 1] = e
return b
我们验证一下例 4 中的容量变化,见例 5.
- 例 5
package main
import "fmt"
func main() {
var a []int = []int{1, 2, 3}
fmt.Printf("a = %v, len = %d, cap = %d\n", a, len(a), cap(a))
b := append(a, 1)
fmt.Printf("a = %v, len = %d, cap = %d\n", a, len(a), cap(a))
fmt.Printf("b = %v, len = %d, cap = %d\n", b, len(b), cap(b))
}
图4 apend 返回的新 slice 容量翻倍
当然了,如果 a 的 cap 足够的话,append 是不会申请新的 slice 的。
3. 切片操作和slice 底层原理
这应该是大家最关心的事情了。实际上,slice 如果用 C 里的 struct 来描述的话,应该像这样:
template <typename T>
struct Slice {
T* data; // 一个指向数据域的指针,指向数组某个元素的地址,不一定非得是首元素。
int len; // 元素的个数
int cap; // 容量大小
};
再来看一新的例子,见例 6.
- 例 6
package main
import "fmt"
func main() {
a := [4]int{0, 1, 2, 3} // a 是一个数组
x := a[0:2] // x 是一个 slice,通过对数组执行切片获得
y := a[0:3] // y 是一个 slice
fmt.Printf("a = %v, len = %d, cap = %d\n", a, len(a), cap(a))
fmt.Printf("x = %v, len = %d, cap = %d\n", x, len(x), cap(x))
fmt.Printf("y = %v, len = %d, cap = %d\n", y, len(y), cap(y))
}
例 6 是另一种创建 slice 的方法,即通过一个已经存在的数组创建出来的。结合刚刚 Slice 结构的定义,x 和 y 的底层结构实际上像图 5 所示。
图5 slice 底层结构
x 和 y,它们指向了相同的底层数组。唯一的区别是,x 和 y 的 len 不一样。不难想象,如果修改了 a 或者 x 或者 y 中的任何一个元素,都会互相产生影响。
3.1 切片操作
上面说对数组执行切片操作,可以生成一个 slice。实际上,对 slice 也可以执行切片操作,它们的语法是相同的。对 slice 做切片,返回的仍然是 slice.
这里使用 gopl 中的例子做说明。见例 7.
- 例 7
package main
import "fmt"
func main() {
months := [...]string{
1: "January",
2: "February",
3: "March",
4: "April",
5: "May",
6: "June",
7: "July",
8: "August",
9: "September",
10: "October",
11: "November",
12: "December", // 后面的逗号不能省略
}
fmt.Printf("months = %v, type = %[1]T, len = %d, cap = %d\n", months, len(months), cap(months))
Q2 := months[4:7]
summer := months[6:9]
fmt.Printf("Q2 = %v, type = %[1]T, len = %d, cap = %d\n", Q2, len(Q2), cap(Q2))
fmt.Printf("summer = %v, type = %[1]T, len = %d, cap = %d\n", summer, len(summer), cap(summer))
}
图6 运行结果
来看一下 Q2 和 summer 这两个 slice 的底层结构:
图7 Q2 和 summer 底层结构
4. 空 slice
空 slice 有两种情况,第一种 slice 变量的值为 nil,第二种,变量的值不为 nil,但是长度为空。
- 例 8
package main
import "fmt"
func main() {
var s1 []int
var s2 []int = nil
var s3 = []int(nil)
var s4 = []int{}
fmt.Printf("%T:%[1]v, %t, len = %d, cap = %d\n", s1, s1 == nil, len(s1), cap(s1)) // []int:[], true, len = 0, cap = 0
fmt.Printf("%T:%[1]v, %t, len = %d, cap = %d\n", s2, s2 == nil, len(s2), cap(s2)) // []int:[], true, len = 0, cap = 0
fmt.Printf("%T:%[1]v, %t, len = %d, cap = %d\n", s3, s3 == nil, len(s3), cap(s3)) // []int:[], true, len = 0, cap = 0
fmt.Printf("%T:%[1]v, %t, len = %d, cap = %d\n", s4, s4 == nil, len(s4), cap(s4)) // []int:[], false, len = 0, cap = 0
}
- s1, s2, s3 虽然都是 nil,但是仍然可以使用 len 和 cap 对其进行求值。
- 可以看到,s4 虽然不等于 nil,但是 len 和 cap 的结果也是 0.
因此,要判断一个 slice 是否为空的最好方法是使用 len 计算其长度,而不是判断其是否为 nil。
5. 总结
- 掌握 slice 声明和定义
- 掌握创建 slice 的几种方法(make,切片)
- 掌握数组与 slice 之间的区别
- 掌握 slice 底层原理
- 掌握判断 slice 为空的方法
- 掌握 append 函数底层原理