Go语言slice的那些坑
Posted 飞鸿影的博客
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Go语言slice的那些坑相关的知识,希望对你有一定的参考价值。
Go语言Google开发的适用于多核编程的语言。我感觉它像是C语言的现代版本,简单,并发支持友好,部署轻松。GO语言中保留关键字就只有25个,这也足以说明它的学习成本并不高。
然而,Go语言里面slice这个东西并不简单。初学者容易掉入坑中。此文件就试图把slice给讲解清楚。
下面先讲一下slice的一些基本特性。
1. slice内部有三个变量,分别是:ptr, len, cap
ptr是用来存储数据的数组
cap是ptr数组的长度
len是实际数组的长度
2. 如何在初始化的时候,指定slice的长度?
a := make([]int, 10)
这里make的时候,第2个参数,就是这个slice的长度。
这个时候它的capacity是多少呢?
fmt.Println(cap(a))
这里打印出来是10。
也就是采用这种方式初始化出来的slice,它的capacity就是长度。
3. 如何在初始化的时候,指定slice的capacity?
a := make([]int, 0, 10)
参数0我们刚刚说过,是长度。10,就是其capacity。这样我们的len(a)==0, cap(a)==10。
我们在写代码的时候,如何事先能够预知一个slice的最大长度,就可以通过指定capacity的方式,优化我们的程序。毕竟,就多加了一个参数而已,我们就减少了几次内存分配释放的开销。
4. 如何往一个slice里面添加数据?添加好之后新的slice和老的有什么样的关系?
b := append(a, 1)
往slice a里面添加一个1的方式,就是上面的代码。不过实际使用中,我们常用的是这种方式:
a = append(a, 1)
那现在考虑这一行代码:
b := append(a, 1)
b和a有什么样的关系呢?
这个要看情况了。
如果len(a)+1<=cap(a),这个时候a内部的数组仍足够存储新添加的数据,此时,b的ptr和a的ptr是相同的。此时:b.ptr == a.ptr, len(b) == len(a)+1, cap(b) == cap(a)。
5. 如何对一个slice重新切片?重新切片之后的ptr, len, cap和原来的slice有何关系?
这里重新切片的意思,就是取slice里面的一部分元素。
a := make([]int, 10, 20) b := a[0:5]
如上面这段代码,就是重新切片的。此时b.ptr == a.ptr。b.cap == a.cap。b.len == 5。
好了,了解了上面的基础属性,现在就可以开始练练手了。
1. 看看下面的代码会输出什么:
package main func main() { a := make([]int, 0) b := append(a, 1) _ = append(a, 2) println(b[0]) }
我们往a里面添加了一个1成为了b。这个时候输出的是1,好像没什么问题。那下面这段代码会输出什么呢?
package main func main() { a := make([]int, 0, 10) b := append(a, 1) _ = append(a, 2) println(b[0]) }
嗯,是2。这个我觉得就是使用slice的时候最大的坑。但理解了它们内部的存储方式,也就不难理解为什么是这样子了。
执行完:
b := append(a, 1)
此时b[0]的确是1。但此时b.ptr == a.ptr。因为这个时候cap(a)为10,足以存储新插入的元素1。
执行:
_ = append(a, 2)
时,cap(a)仍然为10,len(a)仍然为0,往a里面插入元素2 ,使得ptr[0]==2。由于b.ptr与a.ptr相同,b里面的数据就被改掉了。
2. 看看下面的代码会输出什么:
package main func main() { a := make([]int, 10, 20) b := a[5:] println(len(b), cap(b)) }
答案是:5 15
输出5是因为a的长度为10,b := a[5:],相当于是对a重新切片,取a第5个元素以后的值。a第5个元素之后还剩下5个值,那len(b)就是5了。
cap(b)为什么为15呢?
因为此时, b.ptr = a.ptr + 5。也就是b内部指针,指向了a.ptr的后面第5个元素。所以此时cap(b)就不能是20了,因为b无法利用a前面的5个元素。
3. 如果避免重新切片之后的新切片,不被修改?如下所示:
package main import ( "fmt" ) func doAppend(a []int) { _ = append(a, 0) } func main() { a := []int{1, 2, 3, 4, 5} doAppend(a[0:2]) fmt.Println(a) }
这段代码会输出:
[1 2 0 4 5]
虽然我们调用doAppend的时候,只把2个元素传入了。但它却把a的第3个元素改掉了。如何避免呢?答案如下:
package main import ( "fmt" ) func doAppend(a []int) { _ = append(a, 0) } func main() { a := []int{1, 2, 3, 4, 5} doAppend(a[0:2:2]) fmt.Println(a) }
就是在对slice重新切片的时候,加入第三个capacity参数。
doAppend(a[0:2:2])
最后的2,就是指定了重新切片之后新的slice的capacity。我们指定它的capacity就是2,所以,doAppend函数进行append操作的时候,发现capacity不够3,就会重新分配内存。这时就不会修改原有slice的内容了。
4. 假设某个函数一定不会被多goroutine同时调用,如何优化函数内部的内存分配?
package main import ( "fmt" ) // 会被调用很多很多次的函数 func concat(a, b, c, d []byte) []byte { r := make([]byte, len(a)+len(b)+len(c)+len(d)) r = append(r, a...) r = append(r, b...) r = append(r, c...) r = append(r, d...) return r } func main() { for i := 0; i < 100; i++ { fmt.Printf("%s\n", concat([]byte("1"), []byte("2"), []byte("3"), []byte("4"))) } }
假如我们的concat函数会被调用很多很多次,每次调用都make一个新的slice,性能会比较低,如何优化呢?不考虑多线程的情况。
package main import ( "fmt" ) var cache = make([]byte, 0, 100) // 会被调用很多很多次的函数 func concat(a, b, c, d []byte) []byte { newLen := len(a)+len(b)+len(c)+len(d) if newLen > cap(cache) { cache = make([]byte, newLen*2) } r := cache[0:0] r = append(r, a...) r = append(r, b...) r = append(r, c...) r = append(r, d...) return r } func main() { for i := 0; i < 100; i++ { fmt.Printf("%s\n", concat([]byte("1"), []byte("2"), []byte("3"), []byte("4"))) } }
很简单,预先分配一个cache。每次调用concat,都使用cache[0:0]作为起始slice。因为新的r与cache共享了ptr和capacity,所以后面的append不会导致重新分配内存,除非预分配的不够用。
以上是关于Go语言slice的那些坑的主要内容,如果未能解决你的问题,请参考以下文章
Go语言技巧之正确高效使用slice(听课笔记总结--简单易懂)
Go语言技巧之正确高效使用slice(听课笔记总结--简单易懂)