Go语言中的slice表示一个具有相同类型元素的可变长序列,语言本身提供了两个操作方法:
- 创建:make([]T,len,cap)
- 追加: append(slice, T ...)
同时slice支持随机访问。本篇文章主要对slice的具体实现进行总结。
1. 数据结构
go语言的slice有三个主要的属性:
- 指针:slice的首地址指针
- 长度:slice中元素的个数
- 容量:由于slice底层结构本身物理空间可能更大,因此该值记录slice实际空间大小。
因此,在golang官网中的Go Slices: usage and internals对slice的描述如下:
A slice is a descriptor of an array segment. It consists of a pointer to the array, the length of the segment, and its capacity (the maximum length of the segment).
slice是一段array,包括了上面的三个部分,他的物理结构如下:
如果我们通过make([]byte,5,5)
创建了一个len=5,cap=5的slice,其物理结构如此:
如果我们仅仅想使用原数组的一部分,例如:
s = s[2:4]
则s的物理结构如此:
但实际上,这两者所引用的是同一块连续的空间,如果我们修改其中一个,另一个也会跟着修改。实际上,slice在go语言中的代码表示为:
type slice struct {
array unsafe.Pointer
len int
cap int
}
我们是如何知道这件事的呢?请看继续阅读该文章。
操作
go语言为slice提供了两个修改类操作:
- 创建
- 追加
接下来我们会对这两个操作进行分析。
1. 创建slice
slice的定义(分配空间)有三种方式:
- 字面量创建:
s := []int{1,2,3}
- 内置函数make创建:
make([]T, len, cap)
- 切取其他数据结构:
s := array[1:2]
还有两种声明方式(不分配空间):
var s []int
s := []int{}
接下来我们通过一组示例代码,查看slice的创建流程,以及上面的定义与声明的区别。
-
字面量创建方式
// main.go package main import "fmt" func main() { s1 := []int{1,2,3} fmt.Println(s1) }
这组代码给出了一个通过字面量方式创建的slice
s1
,我们通过delve工具对这部分代码进行debug。命令行进入到main.go所在目录,键入如下命令:dlv debug # 为main包的main函数第1行即文件第7行打上断点 b main.go:7 # 运行到断点处 c # 对要运行的部分进行反汇编 disassemble
我们就可以看到如下代码:
TEXT main.main(SB) D:/code/Notes/docs/go/list/main.go main.go:6 0x948300 493b6610 cmp rsp, qword ptr [r14+0x10] main.go:6 0x948304 0f86f5000000 jbe 0x9483ff main.go:6 0x94830a 4883ec78 sub rsp, 0x78 main.go:6 0x94830e 48896c2470 mov qword ptr [rsp+0x70], rbp main.go:6 0x948313 488d6c2470 lea rbp, ptr [rsp+0x70] => main.go:7 0x948318* 488d05a1940000 lea rax, ptr [rip+0x94a1] main.go:7 0x94831f 90 nop # 调用runtime的newobject创建一个新的对象 main.go:7 0x948320 e81b53f6ff call $runtime.newobject # 将调用结果(即新slice的地址)存到栈顶中 main.go:7 0x948325 4889442428 mov qword ptr [rsp+0x28], rax # 把1放入slice中 main.go:7 0x94832a 48c70001000000 mov qword ptr [rax], 0x1 # 从栈顶将slice的地址取出放入rcx寄存器中 main.go:7 0x948331 488b4c2428 mov rcx, qword ptr [rsp+0x28] main.go:7 0x948336 8401 test byte ptr [rcx], al # 把2放入slice中 main.go:7 0x948338 48c7410802000000 mov qword ptr [rcx+0x8], 0x2 main.go:7 0x948340 488b4c2428 mov rcx, qword ptr [rsp+0x28] main.go:7 0x948345 8401 test byte ptr [rcx], al # 把3放入slice中 main.go:7 0x948347 48c7411003000000 mov qword ptr [rcx+0x10], 0x3 main.go:7 0x94834f 488b4c2428 mov rcx, qword ptr [rsp+0x28] main.go:7 0x948354 8401 test byte ptr [rcx], al main.go:7 0x948356 eb00 jmp 0x948358 # 最后设置slice的指针,并将len和cap都设置为3 main.go:7 0x948358 48894c2440 mov qword ptr [rsp+0x40], rcx main.go:7 0x94835d 48c744244803000000 mov qword ptr [rsp+0x48], 0x3 main.go:7 0x948366 48c744245003000000 mov qword ptr [rsp+0x50], 0x3
由此可见,使用字面量创建slice时,len和cap都会设置为初始化数据的个数。
可以简单看一下刚才使用的
runtime.newobject()
,该函数在runtime/malloc.go文件
中,代码如下:func newobject(typ *_type) unsafe.Pointer { return mallocgc(typ.size, typ, true) }
本质上还是通过内存管理机制为一个对象申请一块连续空间并返回对应指针。
-
make函数创建:
// main.go package main import "fmt" func main() { s := make([]int, 10,20) fmt.Println(s) }
该例子使用make方式创建了slice
s
,其len=10,cap=20,同样使用delve进行debug,脚本同上,我们得到的反汇编结果如下:TEXT main.main(SB) D:/code/Notes/docs/go/list/main.go main.go:6 0xea8300 493b6610 cmp rsp, qword ptr [r14+0x10] main.go:6 0xea8304 0f86