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语言实践:从新手入门到上线真实的小型服务所遇到的那些坑

Go语言的那些坑

Go语言技巧之正确高效使用slice(听课笔记总结--简单易懂)

Go语言技巧之正确高效使用slice(听课笔记总结--简单易懂)

Go语言技巧之正确高效使用slice(听课笔记总结--简单易懂)

Go语言切片