Featured image of post Go语言——切片(Slice)的坑

Go语言——切片(Slice)的坑

Go语言切片Slice在使用中和数组的区别和程序遇到的一些问题

切片(Slice)

  初学go语言的小伙伴可能在切片和数组(Array)上有很多困惑,而经常出问题使用错误的就是切片slice,首先要知道的是切片的底层结构:

1
2
3
4
5
type slice struct {
	array unsafe.Point // 指向底层数组的指针
	len int // 切片的长度
	cap int // 切片的容量
}

  观察上面的结构,我们可以理解为slice是只是指向一个数组,并不是真的申请了一篇内存来保存数组,比如如果一个底层数组有10个元素,slice可以访问其中的前5个数,这样子slice的地址和数组的头地址就是一样的,而且slice可以共享底层数组。

  而且这里要说明一下,Go语言类型都是值类型,只不过有的类型内部使用是用指针实现,所以可以实现引用,而数组的声明要带有长度,如果声明好了,则长度是不会发生改变的,如果想要扩容那么只能新声明一个新数组,这个和slice完全不同。

1
2
3
4
5
	var a [5]int
	a = append(a, 6)
	b := [5]int{1,2,3,4,5}
	b = append(b, 6)
	// 会有如下错误提示:Cannot use 'a' (type [5]int) as the type []Type

  这里有一点因为上面说到的值类型,如果在函数调用中传参为数组的话,那么没调用一次就会将数组的内存空间大小分配在栈中,如果数组过大那么就会很影响内存的利用,所以slice,利用指向数组的指针,传参用slice就会解决这个问题。

1
2
3
4
5
6
7
func test1(arr [5]int) { 
	...
}

func test2(arr []int) {
	...
}

  slice的声明可以定义好长度和容量,也可以不设定定义为nil或是空切片。

1
2
3
4
	var a []int // 长度和容量均为0,nil切片
	b := []int{1, 2} // 长度和容量均为2
	c := make([]int, 0) // 空切片
	d := make([]int,1, 2)

  由于slice是指向底层数组的指针,且可以共享底层数组,所以两个指向同一个底层数组的切片,对其中一个修改值,会影响另一个

1
2
3
4
5
6
7
8
	a := []int{1, 2, 3, 4, 5, 6}
	b := a[2:4]
	fmt.Println(len(a), cap(a), len(b), cap(b)) // 这里打印的结果是6 6 2 4
	// b的长度计算方式是4-2,容量的计算方式是a的容量6减b其实索引2
	fmt.Println(b) // [3 4]
	b[0] = 10
	fmt.Println(b, a) // [10 4] [1 2 10 4 5 6] 这面由于指向同一个底层数组,所以打印a也会变化
	fmt.Println(b[:3]) // [10 4 5] 因为b的容量是4,b指向从索引2开始,所以打印出来的是[10 4 5],但是不能打印b[3],因为b的长度是2,只能访问长度范围内的索引

  slice相对于数组来说可以直接使用append来扩容增长,如果slice的容量够,append方法不会改变底层数组,不够的话,append 会创建一个新的底层数组,拷贝原先的值和添加新值。但是因为slice是指向数组指针的原因,这其中又有一些问题。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
	a := []int{1, 2, 3, 4, 5, 6}
	b := a[2:4]
	fmt.Println(len(a), cap(a), len(b), cap(b)) // 6 6 2 4
	b = append(b, 10) 
	fmt.Println(b, a) // [3 4 10] [1 2 3 4 10 6]
	fmt.Println(len(a), cap(a), len(b), cap(b)) // 6 6 3 4 b的长度增长了,容量因为没有超出所以没有变化,底层数组也没有变,a[4]发生了变化。
	
	b = append(b, 20)
	fmt.Println(len(a), cap(a), len(b), cap(b)) // 6 6 4 4
	b = append(b, 30)
	fmt.Println(len(a), cap(a), len(b), cap(b)) // 6 6 5 8 超出原先b的容量,b扩容了,此时b的底层数组不是a的底层数组了,扩容机制这个我记得是小于1024个元素容量扩容每次乘2,大于1024时是乘1.25,我并不太确定。
	fmt.Println(b, a) // [3 4 10 20 30] [1 2 3 4 10 20] 
	b[0] = 100
	fmt.Println(b, a) // [100 4 10 20 30] [1 2 3 4 10 20] a 没有发生变化
	
	c := []int{1, 2, 3, 4, 5}
	d := c[2:4:5] // 这种写法就是三种参数的写法,d的长度是2,容量是5-3,这里的第三个参数不能超过c的容量,不然会报错
	fmt.Println(d, len(d), cap(d)) // [3 4] 2 3
	// 这样做限定了容量最大值,如果d扩容超过最大容量,就会创建新的底层数组,不会影响c了

  大体的切片知识就是这样,实际中可能还会有很多问题,前一阵子看到煎鱼大佬得一篇文章,这里把这篇文章的主要内容拿出来聊聊。首先放上煎鱼大佬的文章链接,接下来看一下这段程序,思考一下这段代码的输出是怎么样的?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
	sl := make([]int, 0, 10)
	var appenFunc = func(s []int) {
		s = append(s, 10, 20, 30)
		fmt.Println(s)
	}
	fmt.Println(sl)
	appenFunc(sl)
	fmt.Println(sl)
	fmt.Println(sl[:10])
	
	//[]  
	//[10 20 30]
	//[]
	//[10 20 30 0 0 0 0 0 0 0]

  看到输出的话问题来了,最让人疑惑的就是后两句打印。

1
2
	fmt.Println(sl)
	fmt.Println(sl[:10])

  另外再加一句fmt.Println(sl[:])猜一下打印结果如何?整合一下结果应该如下:

1
2
3
4
5
6
7
	fmt.Println(sl)
	fmt.Println(sl[:10])
	fmt.Println(sl[:])
	
	//[]
	//[10 20 30 0 0 0 0 0 0 0]
	//[]

  更加令人疑惑了?为什么后两句的打印结果不一样呢?再看完了以上切片的介绍,以及知道Go语言是值类型传递的,我们来分析这段程序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
	sl := make([]int, 0, 10)
	var appenFunc = func(s []int) {
		fmt.Println(len(s), cap(s)) // 0 10 没什么争议
		s = append(s, 10, 20, 30)
		fmt.Println(len(s), cap(s)) // 3 10 没什么争议
		fmt.Printf("%p\n", &s) // 0xc000004108 没什么争议,再解释一下Go语言是值类型传递,所以闭包种的s相当于一个副本,所以地址和sl的地址不一样,但是重点!!!s指向的底层数组和sl指向的是一样的,这是解决这些问题的关键所在。
	}
	fmt.Println(sl) // [] 为空,没什么争议
	fmt.Printf("%p\n", &sl) // 0xc0000040d8
	appenFunc(sl) // 看上面咯
	fmt.Println(sl) // [] 为空,因为是值传递所以sl的长度不变,容量也不变,但是再仔细思考一下底层数组变了么?答案是变了
	fmt.Println(sl[:10]) // [10 20 30 0 0 0 0 0 0 0] 没错 底层数组变了,而且sl的容量是10可以打印,所以将底层数组打印出来了
	fmt.Println(sl[:]) // [] 为空,前面说了sl的长度不会改变,所以其实sl的长度依然是0!!!那这么打印就不会有任何结果的
	fmt.Println(len(sl), cap(sl)) // 0 10 到这应该就完全理解了
	
	// 这里也说明一下,比如fmt.Println(sl)为什么打印的不是完整底层数组这种的问题,这其实和fmt包下打印方法的写法有关,这里就不深研究这个了。

  看了上面一步步的分析后,是不是理解了很多,其实环环相扣,清晰了理解起来就很容易,过了一阵子后,煎鱼大佬又又发了篇关于slice的文章,这里也拿出来说一下,是有关slice操作导致内存泄露的问题,还是依然看一下如下程序,判断一下是否会导致内存泄漏:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var a []int

func f(b []int) []int {
	a = b[:2]
	return a
}

func main() {
	...
}

  这里先简单说一下内存泄漏,内存泄露是在程序运行过程中不再使用的内存,没有被释放掉,导致这些内存无法被使用,等到程序结束后才会释放掉,Go语言也有GC垃圾回收机制回收堆上不使用的内存,但代码写法的问题依然会造成内存泄漏。而Go语言的内存泄漏主要是goroutine泄漏,这个之后写博客可以介绍介绍,回到这个问题,上面的这个代码无疑是会造成内存泄漏的,那么问题在哪?答案还是在slice的底层结构上,分析下这段程序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
var a []int // 静态存储变量

func f(b []int) []int {
	// b 是动态存储变量
	a = b[:2]  // 跟着上文的介绍,a b 都指向同一个底层数组,只不过a只包含[:2],b是全部
	return a
}

func main() {
	...
}

  到这其实就很明了了,由于ab指向同一个底层数组,a是静态存储变量被分配了固定的内存空间,如果程序f(b []int)结束了,b这种动态变量应该被回收了,但是由于a还在使用,尽管只是使用两个元素,后面的元素毫无作用,但是依然不会被GC,所以导致了泄漏,煎鱼大佬还给了一个解决方案,也同样是利用了slice的append特性,程序如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var a []int
var c []int    // 第三者

func f(b []int) []int {
	a = b[:2]

	// 新的切片 append 导致切片扩容
	c = append(c, b[:2]...)
	fmt.Printf("a: %p\nc: %p\nb: %p\n", &a[0], &c[0], &b[0])

	return a
}

  这里的ab是指向同一个底层数组,而c是通过没有容量的切片,进行append后重新申请分配空间的变量,因此使用c实现了防止内存泄漏。

  OK!有关slice的部分暂时能想到的就这些,也先介绍这么多啦~

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy