切片本质
切片的定义如下:
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)) }
|
输出如下:
可以看到,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)) }
|
输出如下:
所以,需要记住,切片变量的len和cap发生是跟着切片变量走的,和指向的数组在存储上没有任何关系。
总结
用切片时一定要小心,特别是两个切片指向同一内存区域时,使用append()操作可能会导致对另一个切片的更改,又可能不会更改。还有切片在作为参数时也要小心,如希望函数内外一致的,可以使用切片指针。