文章目录

前几天使用golang时,遇到一个很奇怪的问题,原因是对golang不熟悉,所以记录一下。

在使用append()函数给slice中添加元素时,slice的初始大小可以为0,也就是len可以为0。每次向sliceappend的时候,如果容量cap不够,会自动对slice进行扩容,也就是改变slicecap的大小。

而在使用copy()函数操作slice时,如果slice的大小为0时,会不添加任何元素,不会自动增加slice的容量大小。这里要注意。

下面是从golang官网源码库中看到的实现代码,如下所示:

  • runtime/slice.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
func slicecopy(to, fm slice, width uintptr) int {
if fm.len == 0 || to.len == 0 {
return 0
}

n := fm.len
if to.len < n {
n = to.len
}

if width == 0 {
return n
}

if raceenabled {
callerpc := getcallerpc(unsafe.Pointer(&to))
pc := funcPC(slicecopy)
racewriterangepc(to.array, uintptr(n*int(width)), callerpc, pc)
racereadrangepc(fm.array, uintptr(n*int(width)), callerpc, pc)
}
if msanenabled {
msanwrite(to.array, uintptr(n*int(width)))
msanread(fm.array, uintptr(n*int(width)))
}

size := uintptr(n) * width
if size == 1 { // common case worth about 2x to do here
// TODO: is this still worth it with new memmove impl?
*(*byte)(to.array) = *(*byte)(fm.array) // known to be a byte pointer
} else {
memmove(to.array, fm.array, size)
}
return n
}

从上面的代码可以看出来,如果slicelen值为0,则直接return,不会进行复制操作。

下面是示例代码,验证一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package main

import (
"regexp"
"log"
"unsafe"
)

func init() {
log.SetFlags(log.Lshortfile|log.LstdFlags)
}

func sliceTest(list []int) {
if len(list) > 0 {
log.Println("func pos 0:", &list[0])
}

log.Println("func pointer:", unsafe.Pointer(&list))
log.Println("------")
}

func copyTest() {
list := make([]int, 0)
log.Println("pointer:", unsafe.Pointer(&list))
sliceTest(list)
log.Println("++++++++++++++")

list = append(list, 1)
log.Println("pos 0:", &list[0])
log.Println("pointer:", unsafe.Pointer(&list))
sliceTest(list)
log.Println("++++++++++++++")

list = append(list, 2)
log.Println("pos 0:", &list[0])
log.Println("pointer:", unsafe.Pointer(&list))
sliceTest(list)
log.Println("++++++++++++++")

copy1 := make([]int, 0)
copy(copy1, list)
log.Println("copy1:", copy1)

copy2 := make([]int, 2)
copy(copy2, list)
log.Println("copy2:", copy2)

copy3 := make([]int, 0, 2)
copy(copy3, list)
log.Println("copy3:", copy3)
log.Println()
}


func main() {
copyTest()
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

pointer: 0xc42000a060
func pointer: 0xc42000a080
------
++++++++++++++
pos 0: 0xc4200160d0
pointer: 0xc42000a060
func pos 0: 0xc4200160d0
func pointer: 0xc42000a0a0
------
++++++++++++++
pos 0: 0xc4200160f0
pointer: 0xc42000a060
func pos 0: 0xc4200160f0
func pointer: 0xc42000a0c0
------
++++++++++++++
copy1: []
copy2: [1 2]
copy3: []

从上面的日志可以看出来,copy1和copy3都是空,没有进行复制操作;每次进行扩容的时候,pos 0:的地址都会发生变化。另外还可以看出来,当把slice传给一个函数时,对slice的结构体发生的值传递,而slice中指向数据内容的地址没有变,即上面pos 0:func pos 0:输出的地址是一致。

同样可以在runtime/slice.go源码中的makeslice()函数中可以看到创建的slice结构体,如下所示:

1
2
3
4
5
type slice struct {
array unsafe.Pointer
len int
cap int
}

也就是说当把slice直接传给函数时,会对这个slice结构体内容进行复制,而array的地址不会变,始终指向数组内容的首元素地址。

另外看这个runtime/slice.go的源码时,还发现一个有趣的细节,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func growslice(et *_type, old slice, cap int) slice {
// ...

newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
for newcap < cap {
newcap += newcap / 4
}
}
}

// ...
}

slice进行扩容的时候,如果其容量小于1024,则容量增加一倍;否则容量以1.25倍的数量增加。

以上golang源码是基于1.9.1版本。

文章目录