Go语言切片是怎么扩容的
有志者,事竟成!如果你在学习Golang,那么本文《Go语言切片是怎么扩容的》,就很适合你!文章讲解的知识点主要包括go语言,若是你对本文感兴趣,或者是想搞懂其中某个知识点,就请你继续往下看吧~
本篇内容主要讲解“Go语言切片是怎么扩容的”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Go语言切片是怎么扩容的”吧!
在 Go 语言中,有一个很常用的数据结构,那就是切片(Slice)。
切片是一个拥有相同类型元素的可变长度的序列,它是基于数组类型做的一层封装。它非常灵活,支持自动扩容。
切片是一种引用类型,它有三个属性:指针,长度和容量。
底层源码定义如下:
type slice struct { array unsafe.Pointer len int cap int }
指针: 指向 slice 可以访问到的第一个元素。
长度: slice 中元素个数。
容量: slice 起始元素到底层数组最后一个元素间的元素个数。
比如使用 make([]byte, 5)
创建一个切片,它看起来是这样的:
声明和初始化
切片的使用还是比较简单的,这里举一个例子,直接看代码吧。
func main() { var nums []int // 声明切片 fmt.Println(len(nums), cap(nums)) // 0 0 nums = append(nums, 1) // 初始化 fmt.Println(len(nums), cap(nums)) // 1 1 nums1 := []int{1,2,3,4} // 声明并初始化 fmt.Println(len(nums1), cap(nums1)) // 4 4 nums2 := make([]int,3,5) // 使用make()函数构造切片 fmt.Println(len(nums2), cap(nums2)) // 3 5 }
扩容时机
当切片的长度超过其容量时,切片会自动扩容。这通常发生在使用 append
函数向切片中添加元素时。
扩容时,Go 运行时会分配一个新的底层数组,并将原始切片中的元素复制到新数组中。然后,原始切片将指向新数组,并更新其长度和容量。
需要注意的是,由于扩容会分配新数组并复制元素,因此可能会影响性能。如果你知道要添加多少元素,可以使用 make
函数预先分配足够大的切片来避免频繁扩容。
接下来看看 append
函数,签名如下:
func Append(slice []int, items ...int) []int
append
函数参数长度可变,可以追加多个值,还可以直接追加一个切片。使用起来比较简单,分别看两个例子:
追加多个值:
package main import "fmt" func main() { s := []int{1, 2, 3} fmt.Println("初始切片:", s) s = append(s, 4, 5, 6) fmt.Println("追加多个值后的切片:", s) }
输出结果为:
初始切片: [1 2 3]
追加多个值后的切片: [1 2 3 4 5 6]
再来看一下直接追加一个切片:
package main import "fmt" func main() { s1 := []int{1, 2, 3} fmt.Println("初始切片:", s1) s2 := []int{4, 5, 6} s1 = append(s1, s2...) fmt.Println("追加另一个切片后的切片:", s1) }
输出结果为:
初始切片: [1 2 3]
追加另一个切片后的切片: [1 2 3 4 5 6]
再来看一个发生扩容的例子:
package main import "fmt" func main() { s := make([]int, 0, 3) // 创建一个长度为0,容量为3的切片 fmt.Printf("初始状态: len=%d cap=%d %v\n", len(s), cap(s), s) for i := 1; i <p>输出结果为:</p><blockquote><p>初始状态: len=0 cap=3 []<br>添加元素1: len=1 cap=3 [1]<br>添加元素2: len=2 cap=3 [1 2]<br>添加元素3: len=3 cap=3 [1 2 3]<br>添加元素4: len=4 cap=6 [1 2 3 4]<br>添加元素5: len=5 cap=6 [1 2 3 4 5]</p></blockquote><p>在这个例子中,我们创建了一个长度为 <code>0</code>,容量为 <code>3</code> 的切片。然后,我们使用 <code>append</code> 函数向切片中添加 <code>5</code> 个元素。</p><p>当我们添加第 <code>4</code> 个元素时,切片的长度超过了其容量。此时,切片会自动扩容。新的容量是原始容量的两倍,即 <code>6</code>。</p><p>表面现象已经看到了,接下来,我们就深入到源码层面,看看切片的扩容机制到底是什么样的。</p><h3>源码分析</h3><p>在 Go 语言的源码中,切片扩容通常是在进行切片的 <code>append</code> 操作时触发的。在进行 <code>append</code> 操作时,如果切片容量不足以容纳新的元素,就需要对切片进行扩容,此时就会调用 <code>growslice</code> 函数进行扩容。</p><p><code>growslice</code> 函数定义在 Go 语言的 runtime 包中,它的调用是在编译后的代码中实现的。具体来说,当执行 <code>append</code> 操作时,编译器会将其转换为类似下面的代码:</p><pre class="brush:go;">slice = append(slice, elem)
在上述代码中,如果切片容量不足以容纳新的元素,则会调用 growslice
函数进行扩容。所以 growslice
函数的调用是由编译器在生成的机器码中实现的,而不是在源代码中显式调用的。
切片扩容策略有两个阶段,go1.18 之前和之后是不同的,这一点在 go1.18 的 release notes 中有说明。
下面我用 go1.17 和 go1.18 两个版本来分开说明。先通过一段测试代码,直观感受一下两个版本在扩容上的区别。
package main import "fmt" func main() { s := make([]int, 0) oldCap := cap(s) for i := 0; i %4d] cap = %-4d | after append %-4d cap = %-4d\n", 0, i-1, oldCap, i, newCap) oldCap = newCap } } }
上述代码先创建了一个空的 slice,然后在一个循环里不断往里面 append
新元素。
然后记录容量的变化,每当容量发生变化的时候,记录下老的容量,添加的元素,以及添加完元素之后的容量。
这样就可以观察,新老 slice 的容量变化情况,从而找出规律。
运行结果(1.17 版本):
[0 -> -1] cap = 0 | after append 0 cap = 1
[0 -> 0] cap = 1 | after append 1 cap = 2
[0 -> 1] cap = 2 | after append 2 cap = 4
[0 -> 3] cap = 4 | after append 4 cap = 8
[0 -> 7] cap = 8 | after append 8 cap = 16
[0 -> 15] cap = 16 | after append 16 cap = 32
[0 -> 31] cap = 32 | after append 32 cap = 64
[0 -> 63] cap = 64 | after append 64 cap = 128
[0 -> 127] cap = 128 | after append 128 cap = 256
[0 -> 255] cap = 256 | after append 256 cap = 512
[0 -> 511] cap = 512 | after append 512 cap = 1024
[0 -> 1023] cap = 1024 | after append 1024 cap = 1280
[0 -> 1279] cap = 1280 | after append 1280 cap = 1696
[0 -> 1695] cap = 1696 | after append 1696 cap = 2304
运行结果(1.18 版本):
[0 -> -1] cap = 0 | after append 0 cap = 1
[0 -> 0] cap = 1 | after append 1 cap = 2
[0 -> 1] cap = 2 | after append 2 cap = 4
[0 -> 3] cap = 4 | after append 4 cap = 8
[0 -> 7] cap = 8 | after append 8 cap = 16
[0 -> 15] cap = 16 | after append 16 cap = 32
[0 -> 31] cap = 32 | after append 32 cap = 64
[0 -> 63] cap = 64 | after append 64 cap = 128
[0 -> 127] cap = 128 | after append 128 cap = 256
[0 -> 255] cap = 256 | after append 256 cap = 512
[0 -> 511] cap = 512 | after append 512 cap = 848
[0 -> 847] cap = 848 | after append 848 cap = 1280
[0 -> 1279] cap = 1280 | after append 1280 cap = 1792
[0 -> 1791] cap = 1792 | after append 1792 cap = 2560
根据上面的结果还是能看到区别的,具体扩容策略下面边看源码边说明。
go1.17
扩容调用的是 growslice
函数,我复制了其中计算新容量部分的代码。
// src/runtime/slice.go func growslice(et *_type, old slice, cap int) slice { // ... newcap := old.cap doublecap := newcap + newcap if cap > doublecap { newcap = cap } else { if old.cap <p>在分配内存空间之前需要先确定新的切片容量,运行时根据切片的当前容量选择不同的策略进行扩容:</p>
如果期望容量大于当前容量的两倍就会使用期望容量;
如果当前切片的长度小于 1024 就会将容量翻倍;
如果当前切片的长度大于等于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量;
go1.18
// src/runtime/slice.go func growslice(et *_type, old slice, cap int) slice { // ... newcap := old.cap doublecap := newcap + newcap if cap > doublecap { newcap = cap } else { const threshold = 256 if old.cap <p>和之前版本的区别,主要在扩容阈值,以及这行代码:<code>newcap += (newcap + 3*threshold) / 4</code>。</p><p>在分配内存空间之前需要先确定新的切片容量,运行时根据切片的当前容量选择不同的策略进行扩容:</p>
如果期望容量大于当前容量的两倍就会使用期望容量;
如果当前切片的长度小于阈值(默认 256)就会将容量翻倍;
如果当前切片的长度大于等于阈值(默认 256),就会每次增加 25% 的容量,基准是
newcap + 3*threshold
,直到新容量大于期望容量;
内存对齐
分析完两个版本的扩容策略之后,再看前面的那段测试代码,就会发现扩容之后的容量并不是严格按照这个策略的。
那是为什么呢?
实际上,growslice
的后半部分还有更进一步的优化(内存对齐等),靠的是 roundupsize
函数,在计算完 newcap
值之后,还会有一个步骤计算最终的容量:
capmem = roundupsize(uintptr(newcap) * ptrSize) newcap = int(capmem / ptrSize)
到此,相信大家对“Go语言切片是怎么扩容的”有了更深的了解,不妨来实际操作一番吧!这里是golang学习网,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!
以上就是本文的全部内容了,是否有顺利帮助你解决问题?若是能给你带来学习上的帮助,请大家多多支持golang学习网!更多关于Golang的相关知识,也可关注golang学习网公众号。

- 上一篇
- Go语言中的sync.Pool怎么使用

- 下一篇
- mysql left join的基本用法及on与where的区别是什么
-
- 腼腆的可乐
- 这篇文章内容出现的刚刚好,太细致了,太给力了,收藏了,关注博主了!希望博主能多写Golang相关的文章。
- 2023-05-11 21:12:20
-
- 雪白的火车
- 受益颇多,一直没懂这个问题,但其实工作中常常有遇到...不过今天到这,帮助很大,总算是懂了,感谢楼主分享技术文章!
- 2023-05-11 07:32:34
-
- Golang · Go教程 | 20秒前 |
- Golang反射调用方法详解教程
- 378浏览 收藏
-
- Golang · Go教程 | 6分钟前 |
- Go语言错误处理与panicrecover技巧
- 193浏览 收藏
-
- Golang · Go教程 | 12分钟前 |
- Golang访问者模式应用场景解析
- 273浏览 收藏
-
- Golang · Go教程 | 16分钟前 |
- Golang反射原理与reflect包详解
- 165浏览 收藏
-
- Golang · Go教程 | 18分钟前 |
- GolangUDP通信:net.DialUDP使用教程
- 385浏览 收藏
-
- Golang · Go教程 | 21分钟前 |
- Golang性能慢?分析与优化全攻略
- 287浏览 收藏
-
- Golang · Go教程 | 22分钟前 |
- Golang通道与闭包实现观察者模式
- 281浏览 收藏
-
- Golang · Go教程 | 24分钟前 |
- Go语言Ascii85解码技巧与字符处理详解
- 386浏览 收藏
-
- Golang · Go教程 | 26分钟前 | golang 事件 事件驱动架构 NATSStreaming 可靠传递
- Golang事件驱动架构与NATS集成教程
- 401浏览 收藏
-
- Golang · Go教程 | 33分钟前 |
- Golang云原生策略,OPARego集成解析
- 432浏览 收藏
-
- Golang · Go教程 | 36分钟前 |
- Golangselect原理与case随机执行解析
- 295浏览 收藏
-
- Golang · Go教程 | 37分钟前 |
- OpenBSD部署Golang及libc解决方法
- 453浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 542次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 511次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 498次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 484次学习
-
- 边界AI平台
- 探索AI边界平台,领先的智能AI对话、写作与画图生成工具。高效便捷,满足多样化需求。立即体验!
- 418次使用
-
- 免费AI认证证书
- 科大讯飞AI大学堂推出免费大模型工程师认证,助力您掌握AI技能,提升职场竞争力。体系化学习,实战项目,权威认证,助您成为企业级大模型应用人才。
- 424次使用
-
- 茅茅虫AIGC检测
- 茅茅虫AIGC检测,湖南茅茅虫科技有限公司倾力打造,运用NLP技术精准识别AI生成文本,提供论文、专著等学术文本的AIGC检测服务。支持多种格式,生成可视化报告,保障您的学术诚信和内容质量。
- 561次使用
-
- 赛林匹克平台(Challympics)
- 探索赛林匹克平台Challympics,一个聚焦人工智能、算力算法、量子计算等前沿技术的赛事聚合平台。连接产学研用,助力科技创新与产业升级。
- 662次使用
-
- 笔格AIPPT
- SEO 笔格AIPPT是135编辑器推出的AI智能PPT制作平台,依托DeepSeek大模型,实现智能大纲生成、一键PPT生成、AI文字优化、图像生成等功能。免费试用,提升PPT制作效率,适用于商务演示、教育培训等多种场景。
- 570次使用
-
- Go 语言中的空接口(推荐)
- 2023-02-19 483浏览
-
- 详解go语言 make(chan int, 1) 和 make (chan int) 的区别
- 2023-01-07 113浏览
-
- Go语言开发区块链只需180行代码(推荐)
- 2023-01-01 470浏览
-
- 五步让你成为GO 语言高手
- 2023-01-01 345浏览
-
- GO语言(golang)基础知识
- 2023-01-27 459浏览