这个要说明白可能得很长的文章,所以,这里就简单点来说吧,关于数组:

  1. 长度是固定的
  2. 不推荐你直接将数组拿去做参数传递,但是可以传指针,这样就不需要复制
  3. 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部分组成:

  1. 一个数组
  2. 指向这个数组的指针
  3. 长度(length)
  4. 容量(capacity)

这样我们可以用c语言的struct来表示这个slice:

cstruct Slice { 

        int *ptr;

        unsigned len;  

        unsigned cap;

}
ptrlencap
slice

为了验证上面的分析,我们继续反汇编函数f():

bashDump 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
bashesp -- 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)

继续分析上面的指令:

bash9   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代码直接写在汇编指令后面

bash17   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代码来表示上面这几行汇编指令大概就是这个样子 :

bashr.len = y - x;

r.cap = s.cap -x;

r.ptr = s.ptr + x; //  注意这里 ptr 是 int 型指针

从上面的分析可以看到slice操作只有9-24这16条指令,所以说是相当迅速的,这也验证了官方文档说的slice is cheap!