Golang是如何实现动态数组功能的?Slice切片原理解析
Hi 亲爱的朋友们,我是 k 哥。今天,咱们聊一聊Golang 切片。
当我们需要使用数组,但是又不能提前定义数组大小时,可以使用golang的动态数组结构,slice切片。在 Go 语言的众多特性里,slice 是我们经常用到的数据结构。但您有没有想过,它在背后是怎么工作的呢?接下来,咱们就一起仔仔细细地研究研究 slice 的底层到底是咋回事。比如它的底层数据结构是咋样的,又是怎么和数组配合实现动态数组功能的。把这些弄明白了,咱们写代码不光能更高效,还能躲开不少容易出错的地方。
原理
数据结构
我们每定义一个slice变量,golang底层都会构建一个slice结构的对象。slice结构体由3个成员变量构成:
-
array表示数组指针,数组用于存储数据。
-
len表示切片长度,也就是数组index从0到len-1已存储数据。
-
cap表示切片容量,当切片长度超过最大容量时,需要扩容申请更大长度的数组。
type slice struct {array unsafe.Pointer // 数组指针len int // 切片长度cap int // 切片容量
}

切片扩容
当我们往切片中append时,如果新添加数据会导致切片的len>cap,则会触发扩容。申请容量更大的新数组,并将旧数组数据复制到新数组。
当切片扩容时,新申请的数组长度要满足3个需求:
-
数组要能承载本次数据append进去,这是基本要求。
-
除了1中的基本要求,可以额外多申请一部分空间,防止后续append频繁扩容,引起性能问题。
-
额外申请的空间不能过大,防止内存浪费。
为了满足上述的3个要求,golang底层的扩容策略是如果需要的容量比旧容量大很多,则不申请额外的空间;如果需要的容量比旧容量并没有大很多,则可以多申请一些额外的内存空间。具体策略如下:
-
如果本次append之后,需要的容量大于旧切片容量*2,则新切片容量等于需要的容量。
-
如果旧切片容量<256,则新切片容量为旧切片容量*2。
-
否则,以公式newcap += (newcap + 3*threshold) / 4迭代,直到newcap大于需要的容量为止,将newcap作为新切片容量。
// growslice handles slice growth during append.
// It is passed the slice element type, the old slice, and the desired new minimum capacity,
// and it returns a new slice with at least that capacity, with the old data
// copied into it.
// The new slice's length is set to the old slice's length,
// NOT to the new requested capacity.
// This is for codegen convenience. The old slice's length is used immediately
// to calculate where to write new values during an append.
// 参数cap表示本次append之后需要的切片容量
func growslice(et *_type, old slice, cap int) slice {// 扩容策略,决定扩容后切片容量newcap,也就是需要申请的新数组长度。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}}}// 计算新切片的容量、长度var overflow boolvar lenmem, newlenmem, capmem uintptrlenmem = uintptr(old.len) * et.sizenewlenmem = uintptr(cap) * et.sizecapmem, overflow = math.MulUintptr(et.size, uintptr(newcap))capmem = roundupsize(capmem)newcap = int(capmem / et.size)// 数组内存申请var p unsafe.Pointerif et.ptrdata == 0 {p = mallocgc(capmem, nil, false)} else {p = mallocgc(capmem, et, true)}// 数据复制memmove(p, old.array, lenmem)// 构建新的切片返回return slice{p, old.len, newcap}
}
for 循环的坑
在for和for range循环中,对于循环迭代变量,它的作用域是整个循环。在循环时,会创建一个变量,每次迭代都会把值赋给同一个地址的变量。如果我们的代码有引用这个变量,可能会出现不符合预期的结果。
比如下面对for循环迭代变量i的使用,会输出不符合预期的结果。
func main() {var out []*intfor i := 0; i < 3; i++ {out = append(out, &i)}fmt.Println("Values:", *out[0], *out[1], *out[2]) // 输出 Values: 3 3 3fmt.Println("Addresses:", out[0], out[1], out[2]) // 输出 Addresses: 0x40e020 0x40e020 0x40e020
}
原因是在每次迭代中,我们将变量 i 的地址附加到 out 切片,但由于它是同一个变量,因此我们附加相同的地址,该地址最终包含分配给 i 的最后一个值。解决方案之一是将循环变量复制到新变量中:
for i := 0; i < 3; i++ {
+ i := i // Copy i into a new variable.out = append(out, &i)}
改正之后输出符合预期的结果:
Values: 0 1 2 Addresses: 0x40e024 0x40e028 0x40e032
又比如下面for range循环,对迭代变量v的使用,也会输出不符合预期的结果。
package mainimport "fmt"type User struct {Name stringAge int
}func main() {userMap := make(map[string]*User)users := []User{{Name: "a", Age: 22},{Name: "b", Age: 23},{Name: "c", Age: 21},}for _, v := range users {userMap[v.Name] = &v}for name, user := range userMap {fmt.Println(name, "=>", user.Age, ",Addresses:", &user) // 输出一下user的地址}
}
上面的代码输出不符合预期的结果,三个人的年龄和数据地址变成了相同值:
a => 21 ,Addresses: 0xc000012028 b => 21 ,Addresses: 0xc000012028 c => 21 ,Addresses: 0xc000012028
原因跟for循环一样,在循环时,创建了变量v,后续每次迭代都把值拷贝给变量v,导致循环结束后,拷贝的是最后一个,因此输出的age都是21。解决方案之一也是将循环变量复制到新变量中:
for _, v := range users {
+ temp := vuserMap[v.Name] = &temp
}
注意:为了防止循环迭代变量误用导致bug,在Go 1.22中,循环迭代变量的作用域,不再是循环作用域,而是迭代作用域,每次迭代都会申请一个新变量。Fixing For Loops in Go 1.22
实践经验
-
切片容量预分配。如果提前能预估切片容量,最好提前在make时就分配好容量,避免后续go底层的再次扩容,在一定程度上能提升代码执行效率。
-
注意对slice的循环,看是否需要将循环迭代变量赋值到一个临时变量使用,防止bug。
高频面试题
-
数组和切片的区别 (基本必问)
-
切片的扩容策略是怎样的?
-
for range 的时候它的地址会发生变化么?for 循环遍历 slice 有什么问题?
-
拷贝大切片一定比小切片代价大吗?
对于浅拷贝,比如下面的切片赋值,拷贝大切片和小切片代价一样。原因是浅拷贝a和b共用底层数组,不需要重新申请数组空间,做数组数据迁移,而只需要将a的slice数据结构array、len、cap原样赋值给b的slice。
a:=[]int{1,2}
b:=a

对于深拷贝,比如调用copy函数拷贝,拷贝大切片比小切片代价大。原因是深拷贝a和b底层数组不共用,需要重新申请数组空间,并将a中数组元素复制到b,大切片的数据量大,因此数组申请和数据复制的代价也高一些。
a:=[]int{1,2}
b:=make([]int,0)
copy(b,a)

原文链接:<<Golang是如何实现动态数组功能的?Slice切片原理解析>>
相关文章:
Golang是如何实现动态数组功能的?Slice切片原理解析
Hi 亲爱的朋友们,我是 k 哥。今天,咱们聊一聊Golang 切片。 当我们需要使用数组,但是又不能提前定义数组大小时,可以使用golang的动态数组结构,slice切片。在 Go 语言的众多特性里,slice 是我们经常用到的数…...
SQL注入 报错注入+附加拓展知识,一篇文章带你轻松入门
第5关--------------------------------------------> 前端直接不会显示账号密码的打印;但是在接收前端的数据的那部分后端那里,会看前端传递过来的值是否正确,如果不正确,后端接收值那里就会当MySQL语句执行错误,…...
springboot项目里的包spring-boot-dependencies依赖介绍
springboot项目里的包’spring-boot-dependencies‘依赖 我们一般是在项目的pom dependencyManagement标签里引入spring-boot-dependencies,或者根spring-boot-starter-parent里也是继承了它,也正是因为继承了这个依赖,所以我们在写依赖时才不需要写版本…...
C# 下的限定符运算详解(全部,任意,包含)与示例
文章目录 1.限定符概述2. 全部限定符运算(All)3. 任意限定符运算(Any)4. 包含限定符运算(Contains)总结 当我们在C#编程中需要进行条件判断或集合操作时,限定符(qualifiersÿ…...
消息队列RabbitMQ部分知识
1.简述RabbitMQ的架构设计 RabbitMQ 是一个开源的消息代理,采用了高级消息队列协议(AMQP),其架构设计主要包括以下几个关键组件和概念: 1.消息生产者( Producer): 负责发送消息到…...
看门狗应用编程-I.MX6U嵌入式Linux C应用编程学习笔记基于正点原子阿尔法开发板
看门狗应用编程 看门狗应用编程介绍 看门狗定时器的基本概念 看门狗是一个可以在一定时间内被复位/重置的计数器 如果在规定时间内没有复位,看门狗计时器溢出会对CPU产生复位信号使系统重启 有些看门狗可以只产生中断信号而不会使系统复位 I.MX6UL/I.MX6ULL So…...
Bug 解决 | 本地项目上线后出现错误
目录 一、前言 二、原因分析 1、本地代码误发线上 2、环境差异 3、配置差异 4、资源路径差异 5、API 接口差异 6、用量差异 一、前言 大家好,我是小洪爱分享。在开发上线项目的过程中,我们经常会遇到一种让人头疼的情况。那就是开发好的项目功能…...
为什么我工作 10 年后转行当程序员?逆袭翻盘!
今天文章的主人公暂且称他为 A 君。不过 A 君有点特别,非科班,工作 10 年后才转行 iOS 程序员。今年 36 岁,目前在某行业头部企业任职前端负责人,管理 40 人的前端团队。 废话不多说,我们开始 A 君(为了描…...
见证中国数据库的崛起:从追赶到引领的壮丽征程《四》
见证中国数据库的崛起:从追赶到引领的壮丽征程《四》 四、未来展望:中国数据库的机遇与挑战新技术带来的机遇全球化竞争的挑战数据安全与隐私保护的挑战人才培养的持续挑战 【纪录片】中国数据库前世今生 在数字化潮流席卷全球的今天,数据库作…...
OpenCV||超细节的基本操作
一、图像读取 retval cv2.imread(filename[, flags]) filename:需要读取的图片路径名,支持多种图片格式,如JPEG、PNG、TIFF等。flags:一个可选参数,指定加载图像的颜色类型。常用的值包括: cv2.IMGEAD_A…...
算法训练(leetcode)第三十八天 | 1143. 最长公共子序列、1035. 不相交的线、53. 最大子数组和、392. 判断子序列
刷题记录 *1143. 最长公共子序列1035. 不相交的线53. 最大子数组和392. 判断子序列 *1143. 最长公共子序列 leetcode题目地址 本题和718. 最长重复子数组相似,只是本题不要求连续,需要记录前面最长的子序列,在此基础上累计长度。 dp[i][j]…...
STM32——外部中断(EXTI)
目录 前言 一、外部中断基础知识 二、使用步骤 三、固件库实现 四、STM32CubeMX实现 总结 前言 外部中断(External Interrupt,简称EXTI)是微控制器用于响应外部事件的一种方式,当外部事件发生时(如按键按下、传感器信号…...
MySQL多实例部署
1、软件包下载 //环境:一台rocky Linux虚拟机,并且做好的基本配置及时钟同步,使用Xshell连接 [rootmysql ~]# yum -y install tar lrzsz libncurses* libaio perl//将包文件拖进去 [rootmysql ~]# rz -E rz waiting to receive. [rootmysql…...
云开发喝酒小程序3.6全新漂亮UI猜拳喝酒小程序 【已去除流量主】
云开发喝酒小程序3.6全新漂亮UI猜拳喝酒小程序 已去除流量主。UI特别漂亮,实属精品代码。 【已测】云开发喝酒小程序3.6漂亮UI猜拳喝酒小程序 已去除流量主。 云开发(serverless)小程序无需服务器,注册一个小程序就可以直接上线…...
图论进阶之路-最短路(Floyd)
时间复杂度:O(n^3) 使用场景:当需要得知任意两个点的最短距离以及其路径时使用 准备:需要两个矩阵 一个记录最短距离(D) 一个记录最短路径的最后一个结点(P) 其核心在于不断的判断越过中间…...
安装sqllab靶机之后,练习关卡报403 forbidden
解决办法: 在nginx的conf文件中添加上访问index.php vim /usr/local/nginx/conf/nginx.conf 保存退出 再重启一下nginx,就完成了。 ./nginx -s reload...
微信VX多开 免扫码 登录 互斥体 可视化 Exui v1.1 易语言源码附成品软件
UI设计: 1. EXUI界面库20240204 调用的模块: 1. wow64_hook_3.02.ec(压缩包内含) 2. 精易模块[v11.1.0].ec(自行下载) 更新日志: v1.1 2024年7月25日13:28:43 { 1. 有人反馈 设置了V…...
JavaEE 从入门到精通(一) ~ Maven
晚上好,愿这深深的夜色给你带来安宁,让温馨的夜晚抚平你一天的疲惫,美好的梦想在这个寂静的夜晚悄悄成长。 目录 前言 1.1 概念 什么是 Maven? Maven 的核心概念 1.2 maven依赖坐标 1.3 maven仓库 1.4 maven安装 1.5 mave…...
滚珠丝杆与丝杆支撑座:稳定性与精度的双重保障
丝杆支撑座是连接滚珠丝杆与电机的轴承,采用优质的轴承能确保支撑座与滚珠丝杆之间的刚性平衡。那么,滚珠丝杆搭连接杆支撑座有哪些优缺点呢? 正常情况下,丝杆支撑座能够提供稳定的支撑力,确保滚珠丝杆在复杂工况下保持…...
实验5-11 空心的数字金字塔
本题要求实现一个函数,输出n行空心的数字金字塔。 函数接口定义: void hollowPyramid( int n );其中n是用户传入的参数,为[1, 9]的正整数。要求函数按照如样例所示的格式打印出n行空心的数字金字塔,请注意,最后一行的…...
多模态2025:技术路线“神仙打架”,视频生成冲上云霄
文|魏琳华 编|王一粟 一场大会,聚集了中国多模态大模型的“半壁江山”。 智源大会2025为期两天的论坛中,汇集了学界、创业公司和大厂等三方的热门选手,关于多模态的集中讨论达到了前所未有的热度。其中,…...
<6>-MySQL表的增删查改
目录 一,create(创建表) 二,retrieve(查询表) 1,select列 2,where条件 三,update(更新表) 四,delete(删除表…...
JavaScript 中的 ES|QL:利用 Apache Arrow 工具
作者:来自 Elastic Jeffrey Rengifo 学习如何将 ES|QL 与 JavaScript 的 Apache Arrow 客户端工具一起使用。 想获得 Elastic 认证吗?了解下一期 Elasticsearch Engineer 培训的时间吧! Elasticsearch 拥有众多新功能,助你为自己…...
FastAPI 教程:从入门到实践
FastAPI 是一个现代、快速(高性能)的 Web 框架,用于构建 API,支持 Python 3.6。它基于标准 Python 类型提示,易于学习且功能强大。以下是一个完整的 FastAPI 入门教程,涵盖从环境搭建到创建并运行一个简单的…...
UE5 学习系列(三)创建和移动物体
这篇博客是该系列的第三篇,是在之前两篇博客的基础上展开,主要介绍如何在操作界面中创建和拖动物体,这篇博客跟随的视频链接如下: B 站视频:s03-创建和移动物体 如果你不打算开之前的博客并且对UE5 比较熟的话按照以…...
微信小程序 - 手机震动
一、界面 <button type"primary" bindtap"shortVibrate">短震动</button> <button type"primary" bindtap"longVibrate">长震动</button> 二、js逻辑代码 注:文档 https://developers.weixin.qq…...
Linux --进程控制
本文从以下五个方面来初步认识进程控制: 目录 进程创建 进程终止 进程等待 进程替换 模拟实现一个微型shell 进程创建 在Linux系统中我们可以在一个进程使用系统调用fork()来创建子进程,创建出来的进程就是子进程,原来的进程为父进程。…...
在 Spring Boot 中使用 JSP
jsp? 好多年没用了。重新整一下 还费了点时间,记录一下。 项目结构: pom: <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://ww…...
什么是VR全景技术
VR全景技术,全称为虚拟现实全景技术,是通过计算机图像模拟生成三维空间中的虚拟世界,使用户能够在该虚拟世界中进行全方位、无死角的观察和交互的技术。VR全景技术模拟人在真实空间中的视觉体验,结合图文、3D、音视频等多媒体元素…...
实战三:开发网页端界面完成黑白视频转为彩色视频
一、需求描述 设计一个简单的视频上色应用,用户可以通过网页界面上传黑白视频,系统会自动将其转换为彩色视频。整个过程对用户来说非常简单直观,不需要了解技术细节。 效果图 二、实现思路 总体思路: 用户通过Gradio界面上…...
