Go语言中关于切片容量与其底层指针的思考 - Go语言中文社区

Go语言中关于切片容量与其底层指针的思考


Go语言中的切片是常用的一种数据类型,其中切片的底层是数组,切片常用的属性有长度和容量。

其中长度很容易理解,但是容量相对复杂一些。

切片提供了计算容量的函数 cap() 可以测量切片最长可以达到多少:它等于切片的长度 + 数组除切片之外的长度。

以下有几个实例,第一:

slice := []int{10, 20, 30, 40, 50}
newSlice := slice[1:3]
testSlice := slice[1:2]
fmt.Println("cap slice:", cap(slice))
fmt.Println("cap newSlice:", cap(newSlice))
fmt.Println("cap test:", cap(testSlice))

分别打印什么呢?

答案是:

cap slice: 5
cap newSlice: 4
cap test: 4

首先,我们看slice的声明方法,直接声明了一个元素为int的切片,并且赋值,可以理解为底层是一个5个元素的数组,所以slice的容量为5,那么从slice上截取得到的newSlice和testSlice长度不同,为何容量是一样的呢?我们看下边这张图:

其中切片y是x从下标1开始,长度为2,容量为4,指针指向的位置为切片的起始位置,所以不难理解,为什么前一个例子总newSlice与testSlice的容量为何相同了,是因为底层数组的长度是一样的。可以简单记忆为:切片的容量是只与切片的起始下标有关,是底层数组减去起始位置。


再看第二个例子:

图片源自文章: https://www.ardanlabs.com/blog/2013/12/three-index-slices-in-go-12.html

我们常见的切片中只包含一个冒号,用来标识起始位置与结束位置(左闭右开),起始从Go1.2起,支持第三个参数,用来指定该切片的容量,如上图所示,有切片slice[i:j:k],其中切片的长度为j-i,容量为k-i。


接下来是第三个实例:

来源于Stack Overflow https://stackoverflow.com/questions/38573983/capacity-of-slices-in-go

package main

import "fmt"

func main() {
    var a []int
    printSlice("a", a)

    // append works on nil slices.
    a = append(a, 0)
    printSlice("a", a)

    // the slice grows as needed.
    a = append(a, 1)
    printSlice("a", a)

    // we can add more than one element at a time.
    a = append(a, 2, 3, 4)
    printSlice("a", a)
}

func printSlice(s string, x []int) {
    fmt.Printf("%s len=%d cap=%d %vn",
    s, len(x), cap(x), x)
}  

会输出什么呢?

答案是:

a len=0 cap=0 []
a len=1 cap=1 [0]
a len=2 cap=2 [0 1]
a len=5 cap=6 [0 1 2 3 4]      

这里边包含了切片长度扩展的原则:

当切片容量难以满足切片长度时,需要进行扩容,由于增加切片容量需要消耗性能,所以Go默认的是会将切片的容量提高一倍,类似于网络中窗口大小每次也是乘以二。

那么上图中为什么第三次append执行了之后,容量变成了6呢?我认为是由于同时添加了多个元素,所以会根据之前的步长也就是2反复的扩容,直到容量够用,为了验证这个猜想,我们将第三次append改为2,3,4,5,6,7,8,得到的结果是

a len=9 cap=10 [0 1 2 3 4 5 6 7 8]

但是如果我们把多个元素拆开进行append就会验证上边的默认规则,代码如下:

package main

import "fmt"

func main() {
	var a []int
	printSlice("a", a)

	// append works on nil slices.
	a = append(a, 0)
	printSlice("a", a)

	// the slice grows as needed.
	a = append(a, 1)
	printSlice("a", a)

	a = append(a, 2)
	printSlice("a", a)

	a = append(a, 3)
	printSlice("a", a)

	a = append(a, 4)
	printSlice("a", a)
}

func printSlice(s string, x []int) {
	fmt.Printf("%s len=%d cap=%d %vn",
	s, len(x), cap(x), x)
}

打印结果如下:

a len=0 cap=0 []
a len=1 cap=1 [0]
a len=2 cap=2 [0 1]
a len=3 cap=4 [0 1 2]
a len=4 cap=4 [0 1 2 3]
a len=5 cap=8 [0 1 2 3 4]

可以看到,切片的容量是0->1->2->4->8,当切片容量不足时,继续添加元素容量会扩大一倍。

PS: 在Stack Overflow链接中,这里其实是有异议的,在不同的架构/Go版本中,扩容的策略可能会有不同,详情请参考链接中的回复。这里不做过多说明,有不同意见欢迎在评论中讨论。


最后一个实例,其实与之前一篇关于切片是否是传引用调用的博文有关,有兴趣的同学可以翻看一下

https://blog.csdn.net/yuanlaidewo000/article/details/81133350

先看代码:

package main

import (
	"fmt"
)

func main() {
	slice := []int{10, 20, 30, 40, 50}
	newSlice := slice[1:3]
	// 使用原有切片划分出新切片,容量为4
	// 将新元素赋值为 60,会改变底层数组中的元素
	newSlice = append(newSlice, 60) //这里会影响原切片的值,40->60
	fmt.Println(newSlice)
	fmt.Println(slice)
	
	slice1 := []int{10, 20, 30, 40, 50}
	newSlice1 := slice1[1:3:3] // 新切片容量为2
	newSlice1 = append(newSlice1, 60) //这里就不会影响
	fmt.Println(newSlice1)
	fmt.Println(slice1)
}

打印的结果如下:

[20 30 60]
[10 20 30 60 50]
[20 30 60]
[10 20 30 40 50]

为什么第一次会影响原切片,而第二次不会呢?

其实是由于第一次append元素并没有发生扩容,所以其实会修改下标为3的元素,即将40改为了60;但第二次在slice1中,由于切片的容量指定为2,所以再次append的时候发生了扩容,Go会将原来的值拷贝一份并指向新的底层数组,所以append并不会影响原来的切片(底层并不是同一个数组了)。

以上都是切片中的一些细节性问题,如有问题还请各位指正

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/yuanlaidewo000/article/details/81545441
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-03-01 23:25:48
  • 阅读 ( 1658 )
  • 分类:Go

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢