Go语言slice的那些坑

zhanchenxing · · 15801 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

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不会导致重新分配内存,除非预分配的不够用。


差不多了,先写这么多。如何错误敬请指正,还望对您有用。



有疑问加站长微信联系(非本文作者)

本文来自:CSDN博客

感谢作者:zhanchenxing

查看原文:Go语言slice的那些坑

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

15801 次点击  ∙  1 赞  
加入收藏 微博
被以下专栏收入,发现更多相似内容
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传