目录
Go 语言的 slice
很好用,不过也有一些坑。slice
是 Go 语言一个很重要的数据结构。网上已经有很多文章写过了,似乎没必要再写。但是每个人看问题的视角不同,写出来的东西自然也不一样。我这篇会从更底层的汇编语言去解读它。而且在我写这篇文章的过程中,发现绝大部分文章都存在一些问题,文章里会讲到,这里先不展开。
我希望本文可以终结这个话题,下次再有人想和你讨论 slice
,直接把这篇文章的链接丢过去就行了。
当我们在说 slice 时,到底在说什么
slice
翻译成中文就是切片
,它和数组(array)
很类似,可以用下标的方式进行访问,如果越界,就会产生 panic。但是它比数组更灵活,可以自动地进行扩容。
了解 slice 的本质,最简单的方法就是看它的源代码:
// runtime/slice.go
type slice struct {
array unsafe.Pointer // 元素指针
len int // 长度
cap int // 容量
}
看到了吗,slice
共有三个属性:
指针
,指向底层数组;
长度
,表示切片可用元素的个数,也就是说使用下标对 slice 的元素进行访问时,下标不能超过 slice 的长度;
容量
,底层数组的元素个数,容量 >= 长度。在底层数组不进行扩容的情况下,容量也是 slice 可以扩张的最大限度。
注意,底层数组是可以被多个 slice 同时指向的,因此对一个 slice 的元素进行操作是有可能影响到其他 slice 的。
slice 的创建
创建 slice 的方式有以下几种:
序号 | 方式 | 代码示例 |
---|---|---|
1 | 直接声明 | var slice []int |
2 | new | slice := *new([]int) |
3 | 字面量 | slice := []int{1,2,3,4,5} |
4 | make | slice := make([]int, 5, 10) |
5 | 从切片或数组“截取” | slice := array[1:5] 或 slice := sourceSlice[1:5] |
直接声明
第一种创建出来的 slice 其实是一个 nil slice
。它的长度和容量都为0。和nil
比较的结果为true
。
这里比较混淆的是empty slice
,它的长度和容量也都为0,但是所有的空切片的数据指针都指向同一个地址 0xc42003bda0
。空切片和 nil
比较的结果为false
。
它们的内部结构如下图:
创建方式 | nil切片 | 空切片 |
---|---|---|
方式一 | var s1 []int | var s2 = []int{} |
方式二 | var s4 = *new([]int) | var s3 = make([]int, 0) |
长度 | 0 | 0 |
容量 | 0 | 0 |
和 nil 比较 |
true |
false |
nil
切片和空切片很相似,长度和容量都是0,官方建议尽量使用 nil
切片。
关于nil slice
和empty slice
的探索可以参考公众号“码洞”作者老钱写的一篇文章《深度解析 Go 语言中「切片」的三种特殊状态》,地址附在了参考资料部分。
字面量
比较简单,直接用初始化表达式
创建。
package main
import "fmt"
func main() {
s1 := []int{0, 1, 2, 3, 8: 100}
fmt.Println(s1, len(s1), cap(s1))
}
运行结果:
[0 1 2 3 0 0 0 0 100] 9 9
唯一值得注意的是上面的代码例子中使用了索引号,直接赋值,这样,其他未注明的元素则默认 0 值
。
make
make
函数需要传入三个参数:切片类型,长度,容量。当然,容量可以不传,默认和长度相等。
上篇文章《走进Go的底层》中,我们学到了汇编这个工具,这次我们再次请出汇编来更深入地看看slice
。如果没看过上篇文章,建议先回去看完,再继续阅读本文效果更佳。
先来一小段玩具代码,使用 make
关键字创建 slice
:
package main
import "fmt"
func main() {
slice := make([]int, 5, 10) // 长度为5,容量为10
slice[2] = 2 // 索引为2的元素赋值为2
fmt.Println(slice)
}
执行如下命令,得到 Go 汇编代码:
go tool compile -S main.go
我们只关注main函数:
0x0000 00000 (main.go:5)TEXT "".main(SB), $96-0
0x0000 00000 (main.go:5)MOVQ (TLS), CX
0x0009 00009 (main.go:5)CMPQ SP, 16(CX)
0x000d 00013 (main.go:5)JLS 228
0x0013 00019 (main.go:5)SUBQ $96, SP
0x0017 00023 (main.go:5)MOVQ BP, 88(SP)
0x001c 00028 (main.go:5)LEAQ 88(SP), BP
0x0021 00033 (main.go:5)FUNCDATA $0, gclocals·69c1753bd5f81501d95132d08af04464(SB)
0x0021 00033 (main.go:5)FUNCDATA $1, gclocals·57cc5e9a024203768cbab1c731570886(SB)
0x0021 00033 (main.go:5)LEAQ type.int(SB), AX
0x0028 00040 (main.go:6)MOVQ AX, (SP)
0x002c 00044 (main.go:6)MOVQ $5, 8(SP)
0x0035 00053 (main.go:6)MOVQ $10, 16(SP)
0x003e 00062 (main.go:6)PCDATA $0, $0
0x003e 00062 (main.go:6)CALL runtime.makeslice(SB)
0x0043 00067 (main.go:6)MOVQ 24(SP), AX
0x0048 00072 (main.go:6)MOVQ 32(SP), CX
0x004d 00077 (main.go:6)MOVQ 40(SP), DX
0x0052 00082 (main.go:7)CMPQ CX, $2
0x0056 00086 (main.go:7)JLS 221
0x005c 00092 (main.go:7)MOVQ $2, 16(AX)
0x0064 00100 (main.go:8)MOVQ AX, ""..autotmp_2+64(SP)
0x0069 00105 (main.go:8)MOVQ CX, ""..autotmp_2+72(SP)
0x006e 00110 (main.go:8)MOVQ DX, ""..autotmp_2+80(SP)
0x0073 00115 (main.go:8)MOVQ $0, ""..a