这个要说明白可能得很长的文章,所以,这里就简单点来说吧,关于数组:
- 长度是固定的
- 不推荐你直接将数组拿去做参数传递,但是可以传指针,这样就不需要复制
- Go 还提供了 Slice,这是不会复制的,要说这个 Slice,请接着看:
不像c语言中的数组变量,在golang中,数组变量是值,比如把一个数组传递给一个函数,它传递的是原来数组的拷贝,而不是像c语言中那样传递的是一个指向原来数组的指针,这会导致把大型数组传递给函数的效率比较低,所以官方教程建议我们在编程中更多的使用slice,这个slice更像c中的数组,不过在运行时可以检查是否越界访问,比c中的数组更安全。本文就来分析下golang中slice的实现。我们以一个简单的程序来开始分析:
go
$ cat test.go package main import "fmt" func f(s []int, x, y int) []int { r := s[x:y] return r } func main() { i := []int{1,2,3,4,5} s := f(i, 1, 3) fmt.Println(s, len(s), cap(s)) } $ 8g test.go $ 8l test.8 $ ./8.out [2 3] 2 4
slice s232(capacity)4
gdbs := f(i, 1, 3)
bash
0x08048c97 <+58>: movl $0x5,0x24(%esp) 0x08048c9f <+66>: movl $0x5,0x28(%esp) 0x08048ca7 <+74>: mov %edx,0x20(%esp) 1=> 0x08048cab <+78>: lea 0x20(%esp),%esi 2 0x08048caf <+82>: lea (%esp),%edi 3 0x08048cb2 <+85>: cld 4 0x08048cb3 <+86>: movsl %ds:(%esi),%es:(%edi) 5 0x08048cb4 <+87>: movsl %ds:(%esi),%es:(%edi) 6 0x08048cb5 <+88>: movsl %ds:(%esi),%es:(%edi) 7 0x08048cb6 <+89>: movl $0x1,0xc(%esp) 8 0x08048cbe <+97>: movl $0x3,0x10(%esp) 0x08048cc6 <+105>: call 0x8048c00 <main.f> 0x08048ccb <+110>: lea 0x14(%esp),%ebx 0x08048ccf <+114>: mov %ebx,%esi 0x08048cd1 <+116>: lea 0x2c(%esp),%edi
7,8两行把传递给f()函数的后两个参数1和3放入堆栈, 1-6行把第一个参数i放入堆栈,可以看到slice变量i共占了12个字节, 那我们看看这12个字节里面的内容到底是啥。把断点设置在0x08048cb6:
bash
(gdb)break *0x08048cb6 Breakpoint 2 at 0x8048cb6: file /home/tito/gostudy/test.go, line 12. (gdb) c Continuing. Breakpoint 2, 0x08048cb6 in main.main () at /home/tito/gostudy/test.go:12 12 s := f(i, 1, 3) (gdb)i r eax 0x98029c00 -1744659456 ecx 0x0 0 edx 0x98029c00 -1744659456 ebx 0x80f7168 135229800 esp 0x151f90 0x151f90 ebp 0x3d 0x3d esi 0x151fbc 1384380 edi 0x151f9c 1384348 eip 0x8048cb6 0x8048cb6 <main.main+89> eflags 0x200202 [ IF ID ] cs 0x73 115 ss 0x7b 123 ds 0x7b 123 es 0x7b 123 fs 0x0 0 gs 0x3f 63 (gdb)
0x151f90
bash
(gdb) x /3x 0x151f90 0x151f90: 0x98029c00 0x00000005 0x00000005
从官方教程我们可以知道slice有一个lengh和capacity,再结合上面的go代码可以知道后面两个0x00000005一定lengh和capacity。再看看0x98029c00这个数字,看起来很像一个地址,所以我们看看这个地址开始的几个内存单元中存放的是啥
bash
(gdb) x /8x 0x98029c00 0x98029c00: 0x00000001 0x00000002 0x00000003 0x00000004 0x98029c10: 0x00000005 0x00000000 0x00000000 0x00000000
这不就是数组[1,2,3,4,5]吗,所以到现在我们可以确定一个slice在内存中由4部分组成:
- 一个数组
- 指向这个数组的指针
- 长度(length)
- 容量(capacity)
这样我们可以用c语言的struct来表示这个slice:
c
struct Slice { int *ptr; unsigned len; unsigned cap; }
ptrlencap
slice
为了验证上面的分析,我们继续反汇编函数f():
bash
Dump of assembler code for function main.f: 1 0x08048c00 <+0>: mov %gs:0x0,%ecx 2 0x08048c07 <+7>: mov -0x8(%ecx),%ecx 3 0x08048c0a <+10>: cmp (%ecx),%esp 4 0x08048c0c <+12>: ja 0x8048c1a <main.f+26> 5 0x08048c0e <+14>: xor %edx,%edx 6 0x08048c10 <+16>: mov $0x20,%eax 7 0x08048c15 <+21>: call 0x8049a04 <runtime.morestack> 8 0x08048c1a <+26>: sub $0xc,%esp 9 0x08048c1d <+29>: mov 0x18(%esp),%eax 10 0x08048c21 <+33>: mov 0x1c(%esp),%ecx 11 0x08048c25 <+37>: mov 0x20(%esp),%edx 12 0x08048c29 <+41>: cmp %eax,%edx 13 0x08048c2b <+43>: jbe 0x8048c32 <main.f+50> 14 0x08048c2d <+45>: call 0x805566f <runtime.panicslice> 15 0x08048c32 <+50>: cmp %edx,%ecx 16 0x08048c34 <+52>: ja 0x8048c2d <main.f+45> 17 0x08048c36 <+54>: sub %ecx,%edx 18 0x08048c38 <+56>: mov %edx,0x4(%esp) 19 0x08048c3c <+60>: mov %eax,%edx 20 0x08048c3e <+62>: sub %ecx,%edx 21 0x08048c40 <+64>: mov %edx,0x8(%esp) 22 0x08048c44 <+68>: imul $0x4,%ecx,%ecx 23 0x08048c47 <+71>: add 0x10(%esp),%ecx 24 0x08048c4b <+75>: mov %ecx,(%esp) 25 0x08048c4e <+78>: lea (%esp),%esi 26 0x08048c51 <+81>: lea 0x24(%esp),%edi 27 0x08048c55 <+85>: cld 28 0x08048c56 <+86>: movsl %ds:(%esi),%es:(%edi) 29 0x08048c57 <+87>: movsl %ds:(%esi),%es:(%edi) 30 0x08048c58 <+88>: movsl %ds:(%esi),%es:(%edi) 31 0x08048c59 <+89>: add $0xc,%esp 32 0x08048c5c <+92>: ret End of assembler dump.
gdb dump
1 - 7
sub $0xc,%esp
bash
esp -- esp + 0xc: 垃圾数据 (12bytes) esp + 0xc: 函数f()的返回地址 (4bytes) esp + 0x10: s.ptr(0x98029c00) (4bytes) esp + 0x14: s.len(5) (4bytes) esp + 0x18: s.cap(5) (4bytes) esp + 0x1c: f()的第二个参数x(1) (4bytes) esp + 0x20: f()的第三个参数y(3) (4bytes)
继续分析上面的指令:
bash
9 0x08048c1d <+29>: mov 0x18(%esp),%eax 10 0x08048c21 <+33>: mov 0x1c(%esp),%ecx 11 0x08048c25 <+37>: mov 0x20(%esp),%edx 12 0x08048c29 <+41>: cmp %eax,%edx 13 0x08048c2b <+43>: jbe 0x8048c32 <main.f+50> 14 0x08048c2d <+45>: call 0x805566f <runtime.panicslice> 15 0x08048c32 <+50>: cmp %edx,%ecx 16 0x08048c34 <+52>: ja 0x8048c2d <main.f+45>
y <= s.cap && x <= y && x >=0 && y >=0
下面这几条指令比较直白,我把对应的c代码直接写在汇编指令后面
bash
17 0x08048c36 <+54>: sub %ecx,%edx // y - x, 2 18 0x08048c38 <+56>: mov %edx,0x4(%esp) // r.len = 2 19 0x08048c3c <+60>: mov %eax,%edx 20 0x08048c3e <+62>: sub %ecx,%edx //s.cap - x, 4 21 0x08048c40 <+64>: mov %edx,0x8(%esp) //r.cap = 4 22 0x08048c44 <+68>: imul $0x4,%ecx,%ecx // x * 4,因为每个数组元素占用4bytes 23 0x08048c47 <+71>: add 0x10(%esp),%ecx //s.ptr + x * 4 24 0x08048c4b <+75>: mov %ecx,(%esp) //r.ptr = s.ptr + x * 4
用c代码来表示上面这几行汇编指令大概就是这个样子 :
bash
r.len = y - x; r.cap = s.cap -x; r.ptr = s.ptr + x; // 注意这里 ptr 是 int 型指针
从上面的分析可以看到slice操作只有9-24这16条指令,所以说是相当迅速的,这也验证了官方文档说的slice is cheap!