深入理解GO语言slice类型的容量管理
发布日期:2021-11-18 19:17:17 浏览次数:9 分类:技术文章

本文共 3398 字,大约阅读时间需要 11 分钟。

Slice

Slice(切片)代表变长的序列,序列中每个元素都有相同的类型。一个slice类型一般写作[]T,其中T代表slice中元素的类型;slice的语法和数组很像,只是没有固定长度而已。

数组和slice之间有着紧密的联系。一个slice是一个轻量级的数据结构,提供了访问数组子序列(或者全部)元素的功能,而且slice的底层确实引用一个数组对象。一个slice由三个部分构成:指针、长度和容量。指针指向第一个slice元素对应的底层数组元素的地址,要注意的是slice的第一个元素并不一定就是数组的第一个元素。长度对应slice中元素的数目;长度不能超过容量,容量一般是从slice的开始位置到底层数据的结尾位置。内置的len和cap函数分别返回slice的长度和容量。

Slice的特征

首先让我们定义一个slice

var s []int

这里我们定义slice的元素类型为int,其实slice的元素可以是任何类型[]T,

其中T代表slice中元素的类型;slice的语法和数组很像,只是没有固定长度而已。

在刚开始接触Go的时候并没有太多关注过slice如何管理它的容量(capability),只知道我可以向一个slice任意增减元素。

首先我们来看一个空切片的长度和容量

func main() {
var s []int detail(s)}//打印slice长度和容量的函数func detail(s []int) {
fmt.Printf("length: %d cap: %d \n", len(s), cap(s))}

输出

length: 0	cap: 0

就是说,一个初始切片的长度和容量都是0。那么疑问就来了:既然容量为0那我能向slice中成功添加内容吗?

我们来试试:

func main() {
var s []int s = append(s, 1) detail(s)}

输出

length: 1	cap: 1

可以看到slice自动地为我们扩容了。为了观察slice如何管理容量,接下来我们试着向slice中连续添加10个元素,并时刻关注它的长度和容量之间的关系。

func main() {
var s []int for i := 0; i < 10; i++ {
s = append(s, 1) detail(s) }}

输出

length: 1	cap: 1 length: 2	cap: 2 length: 3	cap: 4 length: 4	cap: 4 length: 5	cap: 8 length: 6	cap: 8 length: 7	cap: 8 length: 8	cap: 8 length: 9	cap: 16 length: 10	cap: 16

这次我们似乎发现了slice管理容量的规律:当元素超过自身容量时,将容量翻倍

为了一探究竟:我们来稍微改造一下我们的函数:

我们定义一个值sCap用来观察切片s的容量,s的容量随着元素个数变化。当s的容量变化时,将它打印出来:

func main() {
var s []int var sCap int for i := 0; i < 500; i++ {
s = append(s, 1) if cap(s) != sCap {
fmt.Println(sCap) sCap = cap(s) } }}

输出

01248163264128256512102412801704256035844608

可以发现当容<1024时,以2倍递增。容量超过1024时,容量变为原来的1.25倍,当原切片的长度(以下简称原长度)大于或等于1024时,Go 语言将会以原容量的 1.25倍作为新容量的基准(以下新容量基准)。新容量基准会被调整(不断地与1.25相乘),直到结果不小于原长度与要追加的元素数量之和(以下简称新长度)。最终,新容量往往会比新长度大一些,当然,相等也是可能的

另外,如果我们一次追加的元素过多,以至于使新长度比原容量的 2 倍还要大,那么新容 量就会以新长度为基准。注意,与前面那种情况一样,最终的新容量在很多时候都要比新容 量基准更大一些。更多细节可参见runtime包中 slice.go 文件里的growslice及相关函数 的具体实现。

append 的实现只是简单的在内存中将旧 slice 复制给新 slice

type slice struct {
array unsafe.Pointer len int cap int}
func growslice(et *_type, old slice, cap int) slice {
... doublecap := newcap + newcap if cap > doublecap {
newcap = cap } else {
if old.len < 1024 {
newcap = doublecap } else {
// Check 0 < newcap to detect overflow // and prevent an infinite loop. for 0 < newcap && newcap < cap {
newcap += newcap / 4 } // Set newcap to the requested cap when // the newcap calculation overflowed. if newcap <= 0 {
newcap = cap } } } ...}

更新

修改函数,观察在slice容量变化时内存地址的变化情况

func main() {
var s []int var sCap int for i := 0; i < 100; i++ {
fmt.Printf("addr: %p\t cap: %v\t len: %v \n", s, sCap, len(s)) s = append(s, 1) sCap = cap(s) }}

输出

addr: 0x0	 cap: 0	 len: 0 addr: 0xc000016090	 cap: 1	 len: 1 addr: 0xc000098000	 cap: 2	 len: 2 addr: 0xc0000180a0	 cap: 4	 len: 3 addr: 0xc0000180a0	 cap: 4	 len: 4 addr: 0xc00001c080	 cap: 8	 len: 5 addr: 0xc00001c080	 cap: 8	 len: 6 addr: 0xc00001c080	 cap: 8	 len: 7 addr: 0xc00001c080	 cap: 8	 len: 8 addr: 0xc00009c000	 cap: 16	 len: 9 addr: 0xc00009c000	 cap: 16	 len: 10 addr: 0xc00009c000	 cap: 16	 len: 11 addr: 0xc00009c000	 cap: 16	 len: 12 addr: 0xc00009c000	 cap: 16	 len: 13 addr: 0xc00009c000	 cap: 16	 len: 14 addr: 0xc00009c000	 cap: 16	 len: 15 addr: 0xc00009c000	 cap: 16	 len: 16 addr: 0xc00009e000	 cap: 32	 len: 17 ...

当容量变化时,地址也发生了变化,它不再是原来的旧切片。

之所以生成了新的切片,是因为原来数组的容量已经达到了最大值,再想扩容, Go 默认会先开一片内存区域,把原来的值拷贝过来,然后再执行 append() 操作。这种情况丝毫不影响原数组。

转载地址:https://blog.csdn.net/weixin_39172380/article/details/89923853 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:程序员常用英文名参考
下一篇:Docker更换国内镜像源

发表评论

最新留言

做的很好,不错不错
[***.243.131.199]2024年03月24日 16时10分36秒