【Golang】Golang进阶系列教程--Go 语言切片是如何扩容的?
文章目录
- 前言
- 声明和初始化
- 扩容时机
- 源码分析
- go1.17
- go1.18
- 内存对齐
- 总结
前言
在 Go 语言中,有一个很常用的数据结构,那就是切片(Slice)。
切片是一个拥有相同类型元素的可变长度的序列,它是基于数组类型做的一层封装。它非常灵活,支持自动扩容。
切片是一种引用类型,它有三个属性:指针,长度和容量。

底层源码定义如下:
type slice struct {array unsafe.Pointerlen intcap int
}
- 指针: 指向 slice 可以访问到的第一个元素。
- 长度: slice 中元素个数。
- 容量: slice 起始元素到底层数组最后一个元素间的元素个数。
比如使用 make([]byte, 5) 创建一个切片,它看起来是这样的:

声明和初始化
切片的使用还是比较简单的,这里举一个例子,直接看代码吧。
func main() {var nums []int // 声明切片fmt.Println(len(nums), cap(nums)) // 0 0nums = append(nums, 1) // 初始化fmt.Println(len(nums), cap(nums)) // 1 1nums1 := []int{1,2,3,4} // 声明并初始化fmt.Println(len(nums1), cap(nums1)) // 4 4nums2 := 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 mainimport "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 mainimport "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 mainimport "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 <= 5; i++ {s = append(s, i) // 向切片中添加元素fmt.Printf("添加元素%d: len=%d cap=%d %v\n", i, len(s), cap(s), s)}
}
输出结果为:
初始状态: len=0 cap=3 []
添加元素1: len=1 cap=3 [1]
添加元素2: len=2 cap=3 [1 2]
添加元素3: len=3 cap=3 [1 2 3]
添加元素4: len=4 cap=6 [1 2 3 4]
添加元素5: len=5 cap=6 [1 2 3 4 5]
在这个例子中,我们创建了一个长度为 0,容量为 3 的切片。然后,我们使用 append 函数向切片中添加 5 个元素。
当我们添加第 4 个元素时,切片的长度超过了其容量。此时,切片会自动扩容。新的容量是原始容量的两倍,即 6。
表面现象已经看到了,接下来,我们就深入到源码层面,看看切片的扩容机制到底是什么样的。
源码分析
在 Go 语言的源码中,切片扩容通常是在进行切片的 append 操作时触发的。在进行 append 操作时,如果切片容量不足以容纳新的元素,就需要对切片进行扩容,此时就会调用 growslice 函数进行扩容。
growslice 函数定义在 Go 语言的 runtime 包中,它的调用是在编译后的代码中实现的。具体来说,当执行 append 操作时,编译器会将其转换为类似下面的代码:
slice = append(slice, elem)
在上述代码中,如果切片容量不足以容纳新的元素,则会调用 growslice 函数进行扩容。所以 growslice 函数的调用是由编译器在生成的机器码中实现的,而不是在源代码中显式调用的。
切片扩容策略有两个阶段,go1.18 之前和之后是不同的,这一点在 go1.18 的 release notes 中有说明。
下面我用 go1.17 和 go1.18 两个版本来分开说明。先通过一段测试代码,直观感受一下两个版本在扩容上的区别。
package mainimport "fmt"func main() {s := make([]int, 0)oldCap := cap(s)for i := 0; i < 2048; i++ {s = append(s, i)newCap := cap(s)if newCap != oldCap {fmt.Printf("[%d -> %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.gofunc growslice(et *_type, old slice, cap int) slice {// ...newcap := old.capdoublecap := newcap + newcapif cap > doublecap {newcap = cap} else {if old.cap < 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}}}// ...return slice{p, old.len, newcap}
}
在分配内存空间之前需要先确定新的切片容量,运行时根据切片的当前容量选择不同的策略进行扩容:
- 如果期望容量大于当前容量的两倍就会使用期望容量;
- 如果当前切片的长度小于 1024 就会将容量翻倍;
- 如果当前切片的长度大于等于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量;
go1.18
// src/runtime/slice.gofunc growslice(et *_type, old slice, cap int) slice {// ...newcap := old.capdoublecap := newcap + newcapif cap > doublecap {newcap = cap} else {const threshold = 256if old.cap < threshold {newcap = doublecap} else {// Check 0 < newcap to detect overflow// and prevent an infinite loop.for 0 < newcap && newcap < cap {// Transition from growing 2x for small slices// to growing 1.25x for large slices. This formula// gives a smooth-ish transition between the two.newcap += (newcap + 3*threshold) / 4}// Set newcap to the requested cap when// the newcap calculation overflowed.if newcap <= 0 {newcap = cap}}}// ...return slice{p, old.len, newcap}
}
和之前版本的区别,主要在扩容阈值,以及这行代码:newcap += (newcap + 3*threshold) / 4。
在分配内存空间之前需要先确定新的切片容量,运行时根据切片的当前容量选择不同的策略进行扩容:
- 如果期望容量大于当前容量的两倍就会使用期望容量;
- 如果当前切片的长度小于阈值(默认 256)就会将容量翻倍;
- 如果当前切片的长度大于等于阈值(默认 256),就会每次增加 25% 的容量,基准是 newcap + 3*threshold,直到新容量大于期望容量;
内存对齐
分析完两个版本的扩容策略之后,再看前面的那段测试代码,就会发现扩容之后的容量并不是严格按照这个策略的。
那是为什么呢?
实际上,growslice 的后半部分还有更进一步的优化(内存对齐等),靠的是 roundupsize 函数,在计算完 newcap 值之后,还会有一个步骤计算最终的容量:
capmem = roundupsize(uintptr(newcap) * ptrSize)
newcap = int(capmem / ptrSize)
这个函数的实现就不在这里深入了,先挖一个坑,以后再来补上。
总结
切片扩容通常是在进行切片的 append 操作时触发的。在进行 append 操作时,如果切片容量不足以容纳新的元素,就需要对切片进行扩容,此时就会调用 growslice 函数进行扩容。
切片扩容分两个阶段,分为 go1.18 之前和之后:
一、go1.18 之前:
- 如果期望容量大于当前容量的两倍就会使用期望容量;
- 如果当前切片的长度小于 1024 就会将容量翻倍;
- 如果当前切片的长度大于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量;
二、go1.18 之后:
- 如果期望容量大于当前容量的两倍就会使用期望容量;
- 如果当前切片的长度小于阈值(默认 256)就会将容量翻倍;
- 如果当前切片的长度大于等于阈值(默认 256),就会每次增加 25% 的容量,基准是 newcap + 3*threshold,直到新容量大于期望容量;
相关文章:
【Golang】Golang进阶系列教程--Go 语言切片是如何扩容的?
文章目录 前言声明和初始化扩容时机源码分析go1.17go1.18内存对齐 总结 前言 在 Go 语言中,有一个很常用的数据结构,那就是切片(Slice)。 切片是一个拥有相同类型元素的可变长度的序列,它是基于数组类型做的一层封装…...
【数据结构】顺序表(SeqList)(增、删、查、改)详解
一、顺序表的概念和结构 1、顺序表的概念: 顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。 2、顺序表的结构: (1)静态顺序表:使…...
[golang gin框架] 42.Gin商城项目-微服务实战之后台Rbac微服务角色增删改查微服务
一.重构后台Rbac用户登录微服务功能 上一节讲解了后台Rbac微服务用户登录功能以及Gorm数据库配置单独抽离,Consul配置单独抽离,这一节讲解后台Rbac微服务角色增删改查微服务功能,Rbac微服务角色增删改查微服务和后台Rbac用户登录微服务是属于…...
项目篇:Echo论坛系统项目
一、登录注册模块 1、注册功能 1.1、注册流程图 1.2、注册代码 /*** 用户注册* param user* return Map<String, Object> 返回错误提示消息,如果返回的 map 为空,则说明注册成功*/public Map<String, Object> register(User user) {Map&l…...
数据可视化(2)
1.柱状图 #柱状图 #bar(x,height,width,*,aligncenter,**kwargs) #height柱子的高度,即y轴上的数据 #width数组的宽度,默认值0.8 #*表示后面的参数为匿名关键字,必须传入参数 #kwargs关键字参数x[1,2,3,4,5] height[random.randint(10,100)f…...
MD-MTSP:斑马优化算法ZOA求解多仓库多旅行商问题MATLAB(可更改数据集,旅行商的数量和起点)
一、斑马优化算法ZOA 斑马优化算法(Zebra Optimization Algorithm,ZOA)Eva Trojovsk等人于2022年提出,其模拟斑马的觅食和对捕食者攻击的防御行为。斑马优化算法(Zebra Optimization Algorithm,ZOA&#x…...
【笔试强训选择题】Day32.习题(错题)解析
作者简介:大家好,我是未央; 博客首页:未央.303 系列专栏:笔试强训选择题 每日一句:人的一生,可以有所作为的时机只有一次,那就是现在!! 文章目录 前言 一、Da…...
抖音seo账号矩阵系统源码如何开发布局?
目录 一、 抖音SEO账号矩阵系统源码的开发布局步骤如下: 二。 开发部署源码 三、 开发部署功能设计 1. 短视频AI智能创作 2. 托管式账号管理: 3. 数据分析 4. 智能营销获客 四。 抖音seo源码开发部署交付技术文档包含 五。 开发代码展示: 一、 抖…...
vue项目cdn打包优化
0.用vue ui可以查看项目打包后的情况。 1.定义包的排除 let externals {axios: axios,element-ui: ELEMENT,echarts: echarts,} configureWebpack: {externals: externals }2.配置cdn包资源 // 配置 let cdn {css: [// element-ui csshttps://unpkg.com/element-ui/lib/th…...
Android 之 MediaPlayer 播放音频与视频
本节引言: 本节带来的是Android多媒体中的——MediaPlayer,我们可以通过这个API来播放音频和视频 该类是Androd多媒体框架中的一个重要组件,通过该类,我们可以以最小的步骤来获取,解码 和播放音视频。它支持三种不同的…...
React中事件处理器的基本使用
在React中,为了提高性能,跨浏览器兼容性和开发体验,React实现了一套自己的事件机制,利用事件委托和合成事件的方式统一管理事件订阅和分发。 为了让组件能够响应用户的交互行为,React提供了一系列的事件处理器…...
RobotFramework
一、RobotFramework的简介和特点 1、关键字驱动: 把项目中的业务逻辑封装成一个一个的关键字,然后调用不同的关键字组成不同的业务 2、数据驱动 把测试数据放到excel:yaml文件中 通过改变文件中的数据去驱动测试用例执行 3、特点ÿ…...
【Matplotlib 绘制折线图】
使用 Matplotlib 绘制折线图 在数据可视化中,折线图是一种常见的图表类型,用于展示随着变量的变化,某个指标的趋势或关系。Python 的 Matplotlib 库为我们提供了方便易用的功能来绘制折线图。 绘制折线图 下面的代码展示了如何使用 Matplo…...
ARM汇编基本变量的定义和使用
一、ARM汇编中基本变量是什么? 数字变量: GBLA LCLA SETA 逻辑变量:GBLL LCLL SETL 字符串:GBLS LCLS SETLS 注意需要TAB键定义变量和行首改变值 二、使用步骤 1.引入库 代码如下(示例): GBLA led_num Reset_Handler PROCEXPORT Reset_Handler [WEA…...
排序算法汇总
每日一句:你的日积月累终会成为别人的望尘莫及 目录 常数时间的操作 选择排列 冒泡排列 【异或运算】 面试题: 1)在一个整形数组中,已知只有一种数出现了奇数次,其他的所有数都出现了偶数次,怎么找到…...
cocos2d 中UserDefault在windows平台下的路径问题
在使用cocos2dx c开发项目时,通常使用cocos自带的UserDefault来存储一些项目所用到的一些配置信息:如游戏的音量,游戏的闯关数等... 但是windows平台下,测试发现如果用户的帐户名使用是中文,在启动程序时会报错&#…...
ChatGPT与高等教育变革:价值、影响及未来发展
最近一段时间,ChatGPT吸引了社会各界的目光,它可以撰写会议通知、新闻稿、新年贺信,还可以作诗、写文章,甚至可以撰写学术论文。比尔盖茨、马斯克等知名人物纷纷为此发声,谷歌、百度等知名企业纷纷宣布要提供类似产品。…...
Matlab Image Processing toolbox 下载安装方法
当安装好Matlab之后,发现没有Image Processing toolbox这个图像处理工具箱 从新安装一遍, 选上 Image Processing toolbox 但是不用选matlab即可 1.找到之前安装时的Setup安装程序包,按照之前安装Matlab步骤,到选择需要安装的Ma…...
什么是消息键(Key)?如何使用消息键进行消息顺序性保证?
消息键(Key)是Kafka消息的一个可选属性,用于标识消息的逻辑关联关系。每条消息可以携带一个关键字作为其键,这个键可以是字符串、整数等数据类型。 使用消息键可以在Kafka中实现消息的顺序性保证,具体方式如下&#x…...
慎思笃行,兴业致远:金融行业的数据之道
《中庸》中说,“博学之,审问之,慎思之,明辨之,笃行之”。这段话穿越千年,指引着中国千行百业的发展。对于金融行业来说,庞大的数据量可以说是“博学”的来源。但庞大的数据体量,既是…...
Linux应用开发之网络套接字编程(实例篇)
服务端与客户端单连接 服务端代码 #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> …...
华为云AI开发平台ModelArts
华为云ModelArts:重塑AI开发流程的“智能引擎”与“创新加速器”! 在人工智能浪潮席卷全球的2025年,企业拥抱AI的意愿空前高涨,但技术门槛高、流程复杂、资源投入巨大的现实,却让许多创新构想止步于实验室。数据科学家…...
微信小程序之bind和catch
这两个呢,都是绑定事件用的,具体使用有些小区别。 官方文档: 事件冒泡处理不同 bind:绑定的事件会向上冒泡,即触发当前组件的事件后,还会继续触发父组件的相同事件。例如,有一个子视图绑定了b…...
云计算——弹性云计算器(ECS)
弹性云服务器:ECS 概述 云计算重构了ICT系统,云计算平台厂商推出使得厂家能够主要关注应用管理而非平台管理的云平台,包含如下主要概念。 ECS(Elastic Cloud Server):即弹性云服务器,是云计算…...
C++:std::is_convertible
C++标志库中提供is_convertible,可以测试一种类型是否可以转换为另一只类型: template <class From, class To> struct is_convertible; 使用举例: #include <iostream> #include <string>using namespace std;struct A { }; struct B : A { };int main…...
多种风格导航菜单 HTML 实现(附源码)
下面我将为您展示 6 种不同风格的导航菜单实现,每种都包含完整 HTML、CSS 和 JavaScript 代码。 1. 简约水平导航栏 <!DOCTYPE html> <html lang"zh-CN"> <head><meta charset"UTF-8"><meta name"viewport&qu…...
ubuntu系统文件误删(/lib/x86_64-linux-gnu/libc.so.6)修复方案 [成功解决]
报错信息:libc.so.6: cannot open shared object file: No such file or directory: #ls, ln, sudo...命令都不能用 error while loading shared libraries: libc.so.6: cannot open shared object file: No such file or directory重启后报错信息&…...
QT开发技术【ffmpeg + QAudioOutput】音乐播放器
一、 介绍 使用ffmpeg 4.2.2 在数字化浪潮席卷全球的当下,音视频内容犹如璀璨繁星,点亮了人们的生活与工作。从短视频平台上令人捧腹的搞笑视频,到在线课堂中知识渊博的专家授课,再到影视平台上扣人心弦的高清大片,音…...
Java 与 MySQL 性能优化:MySQL 慢 SQL 诊断与分析方法详解
文章目录 一、开启慢查询日志,定位耗时SQL1.1 查看慢查询日志是否开启1.2 临时开启慢查询日志1.3 永久开启慢查询日志1.4 分析慢查询日志 二、使用EXPLAIN分析SQL执行计划2.1 EXPLAIN的基本使用2.2 EXPLAIN分析案例2.3 根据EXPLAIN结果优化SQL 三、使用SHOW PROFILE…...
CSS 工具对比:UnoCSS vs Tailwind CSS,谁是你的菜?
在现代前端开发中,Utility-First (功能优先) CSS 框架已经成为主流。其中,Tailwind CSS 无疑是市场的领导者和标杆。然而,一个名为 UnoCSS 的新星正以其惊人的性能和极致的灵活性迅速崛起。 这篇文章将深入探讨这两款工具的核心理念、技术差…...
