切片本质

切片的定义如下:

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

切片的本质就是一个指向数组的指针,只是切片有len和cap属性标识当前长度和容量。

append()

append()可以在切片后追加内容。如果原切片的容量足够,则在切片后面直接追加内容,注意,是直接追加内容,并不管底层的数组是否被其他切片共用。来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
func main() {
a := []byte("abcdefg")
b := a[0:1]
fmt.Println(&a[0])
fmt.Println(&b[0])
b = append(b, 'x')
fmt.Println(string(a[1]))
fmt.Println(string(b[1]))
fmt.Println(&a[1])
fmt.Println(&b[1])
}

输出如下:

1
2
3
4
5
6
0xc082008268
0xc082008268
x
x
0xc082008269
0xc082008269

显然,b刚开始指向的是a[0],长度为1,追加a[3]后,即b[1]=’x’,因为b[1]的位置即a[1]的位置,切片a也会被改变。所以结论是b和a共用空间时,b的改变会影响a。

现在修改,在b上面多追加几个切片(超出容量):

1
2
3
4
5
6
7
8
9
10
11
func main() {
a := []byte("abcdefg")
b := a[0:1]
fmt.Println("a[0] address:", &a[0])
fmt.Println("b[0] address:", &b[0])
b = append(b, '1', '2', '3', '4', '5', '6', '7', '8')
fmt.Println("a:", string(a))
fmt.Println("b:", string(b))
fmt.Println("a[0] address:", &a[0])
fmt.Println("b[0] address:", &b[0])
}

输出如下:

1
2
3
4
5
6
a[0] address: 0xc082008268
b[0] address: 0xc082008268
a: abcdefg
b: a12345678
a[0] address: 0xc082008268
b[0] address: 0xc0820082d0

可以看到,刚开始b和a还是共用一块地址,之后的添加,b自己新开辟了空间。所以结论是b开辟新空间时,b的改变不会影响a。

再来看下,如果每次新定义b的效果:

1
2
3
4
5
6
7
8
9
func main() {
a := []byte("abcdefg")
b := []byte{}
b = append(b, a[:]...)
fmt.Println(&a[0])
fmt.Println(&b[0])
fmt.Println(string(a))
fmt.Println(string(b))
}

输出如下:

1
2
3
4
0xc082008268
0xc082008290
abcdefg
abcdefg

可以看到,虽然a和b的内容相同,但地址并不一样,说明系统作了内存复制。

切片扩容

最后来看下capacity满的情况下的动态增长:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func fillSlice(s *[]byte) {
diff := cap(*s) - len(*s)
for i := 0; i < diff; i++ {
*s = append(*s, 'a')
}
}
func main() {
b := []byte{'a'}
fmt.Println("len:", len(b), "cap:", cap(b))
b = append(b, 'a')
fmt.Println("len:", len(b), "cap:", cap(b))
for i := 0; i < 15; i++ {
fillSlice(&b)
b = append(b, 'a')
fmt.Println("len:", len(b), "cap:", cap(b))
}
}

代码中fillSlice()会把slice填充满,再追加一个元素便可触发扩容。
输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
len: 1 cap: 1
len: 2 cap: 8
len: 9 cap: 16
len: 17 cap: 32
len: 33 cap: 64
len: 65 cap: 128
len: 129 cap: 256
len: 257 cap: 512
len: 513 cap: 1024
len: 1025 cap: 1280
len: 1281 cap: 1664
len: 1665 cap: 2304
len: 2305 cap: 3072
len: 3073 cap: 4096
len: 4097 cap: 5376
len: 5377 cap: 6912
len: 6913 cap: 8704

https://www.w3cschool.cn/go_internals/go_internals-cyz6282h.html的说法是:
在对slice进行append等操作时,可能会造成slice的自动扩容。其扩容时的大小增长规则是:

  • 如果新的大小是当前大小2倍以上,则大小增长为新大小
  • 否则循环以下操作:如果当前大小小于1024,按每次2倍增长,否则每次按当前大小1/4增长。直到增长的大小超过或等于新大小。

但根据输出情况,到1024后,除了1280,其他并没有出现”按当前大小1/4增长”。这增长方式还有待进一步研究。

切片和切片指针

我们已经了解了切片的定义,当切片作为函数的参数时,其实切片本身是值传递,只是两个切片变量指向的是同一块内存数组。在函数内对内存数组的修改会直接反应到函数外,但切片变量本身作的修改并不会反应到函数外。
来看一个例子,add()会住slice中添加’b’,当然,我们不能让s发生扩容,所以设置capacity为10。

1
2
3
4
5
6
7
8
9
10
11
func add(s []byte) {
s = append(s, 'b')
fmt.Println(string(s))
}
func main() {
s := make([]byte, 0, 10)
s = append(s, 'a')
add(s)
fmt.Println(string(s))
}

输出如下:

1
2
ab
a

可以看到,main()中的s还是原值。但其实,内存中main()函数中的s指向的是’ab’,只不过main()的s的len还是1,而add()中的s的len为2。所以,要在函数中改变切片,一定要注意。
可以使用指针来使函数内外切片保持一致:

1
2
3
4
5
6
7
8
9
10
11
func add(s *[]byte) {
*s = append(*s, 'b')
fmt.Println(string(*s))
}
func main() {
s := make([]byte, 0, 10)
s = append(s, 'a')
add(&s)
fmt.Println(string(s))
}

输出如下:

1
2
ab
ab

所以,需要记住,切片变量的len和cap发生是跟着切片变量走的,和指向的数组在存储上没有任何关系。

总结

用切片时一定要小心,特别是两个切片指向同一内存区域时,使用append()操作可能会导致对另一个切片的更改,又可能不会更改。还有切片在作为参数时也要小心,如希望函数内外一致的,可以使用切片指针。