golang -- slice 底层逻辑

article/2025/8/13 12:13:51

目录

  • 一、前言
  • 二、结构
  • 三、创建
    • 3.1 根据 ` make`创建
    • 3.2 通过数组创建
  • 四、内置append追加元素
    • 4.1 追加元素
    • 4.2 是否扩容
      • 4.2.1 不扩容
      • 4.2.2 扩容
  • 总结

一、前言

前段时间学了go语言基础,过了一遍之后还是差很多,所以又结合几篇不同资料重新学习了一下相关内容,对slice做个总结

二、结构

Slice(切片)是一种非常灵活的数据结构,又称动态数组,依托数组实现,可以方便的进行扩容、传递等


实际上切片的结构是一个结构体,在runtime/slice 包中定义

type slice struct {array unsafe.Pointerlen   intcap   int
}

结构体中包含三个字段

  • array: 指向底层数组的指针
  • len : 切片的长度,指的是切片中实际存在的元素个数
  • cap : 切片的容量,指的是切片中可以容纳的元素个数

根据 slice 的定义不难看出,slice 的底层实际是一个数组,访问切片元素实际上是通过移动指针操作来访问对应下标元素的


三、创建

3.1 根据 make创建

make函数是Go的内置函数,它的作用是为slice、map或chan初始化并返回引用。make仅仅用于创建slice、map和channel,并返回它们的实例。


make 源码

func makeslice(et *_type, len, cap int) unsafe.Pointer {mem, overflow := math.MulUintptr(et.Size_, uintptr(cap))if overflow || mem > maxAlloc || len < 0 || len > cap {// NOTE: Produce a 'len out of range' error instead of a// 'cap out of range' error when someone does make([]T, bignumber).// 'cap out of range' is true too, but since the cap is only being// supplied implicitly, saying len is clearer.// See golang.org/issue/4085.mem, overflow := math.MulUintptr(et.Size_, uintptr(len))if overflow || mem > maxAlloc || len < 0 {panicmakeslicelen()}panicmakeslicecap()}return mallocgc(mem, et, true)
}

这段代码有这几个功能

  1. 计算需要的内存大小(使用MulUintptr防止溢出)
  2. 检查长度和容量有效性
  3. 调用mallocgc分配内存

可以看到,makeslice 接收三个参数,分别为类型、长度、容量,用来判断指针cap 是否溢出,如果溢出则重新分配


make 示例

    slice := make([]int, 5, 10)

这段代码的含义是:给 slice 分配一个 int 类型的底层数组,len(长度) 为5,
cap(容量) 为10

用图来表示其内部逻辑:

在这里插入图片描述

这时候可以通过访问数组下标添加元素

	slice := make([]int, 5, 10)slice[0] = 100slice[1] = 200slice[2] = 300fmt.Println(slice)  //[100 200 300 0 0]

需要注意,下标不可以超过长度,否则会引发panic,例如:

	slice[5] = 500

panic: runtime error: index out of range [5] with length 5

make 的第二个参数 len 可以省略,表示 len = cap

	slice := make([]int, 5)fmt.Println(len(slice))  //5fmt.Println(cap(slice))  //5

3.2 通过数组创建

通过数组创建切片,也就是截取数组中的一部分来作为切片(通过下标截取)

示例:

	array := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}slice1 := array[1:4] //[1 2 3]  len=3  cap=9slice2 := array[:2]  //[0 1]  len=2  cap=10slice3 := array[5:]  //[5 6 7 8 9]  len=5  cap=5

首先创建了一个数组 array ,再通过截取 array 得到切片

切片可以指向同一个底层数组,也可以和数组指向同一个底层数组,所以这三个切片实际上是这样的↓

array

这时,如果改变其中任何一个切片的值,和它共用同一个底层数组的切片和数组都会收到影响(这导致了我们误以为在函数传参的时候切片是引用传递,实际上 go 语言中所有类型都是值传递)


len 和 cap 的计算

示例

1. 基础用法

    slice := array [ start : end : m ]

这段代码表示的含义是:slice 是数组 array 从下标 start 开始,到 end 结束(不包含end)的一段, slice 的长度(len)就是end - start, 容量(cap)是 m - start

2. m省略

如果 m 省略,那么 m = len( array ) ,容量就是 len( array ) - start

    slice := array[ start : end ]

3. start 省略

如果 start 省略,表示从0开始,start = 0

	array := [6]int{1, 2, 3, 4, 5, 6}slice := array[:4]     //[1 2 3 4]  len=4,cap=6

4. end省略

如果end省略,表示到 len( array ) 结束,end = len (array )

	array := [6]int{1, 2, 3, 4, 5, 6}slice := array[3:] //[4 5 6]  len=3,cap=3

四、内置append追加元素

4.1 追加元素

append 定义的源代码在 builtin.go 中

func append(slice []Type, elems ...Type) []Type

append接收两个参数,切片也就是要进行追加操作的切片


1. 追加一个元素

	slice := make([]int, 0, 5)slice = append(slice, 1)fmt.Println(slice)      //[1]fmt.Println(len(slice)) //1fmt.Println(cap(slice)) //5

2. 一次性追加多个元素

	slice := make([]int, 0, 5)slice = append(slice, 1, 2, 3)fmt.Println(slice)      //[1 2 3]fmt.Println(len(slice)) //3fmt.Println(cap(slice)) //5

3. 直接追加一个切片(不可以追加数组)

	slice2 := []int{100, 200, 300}slice := make([]int, 0, 5)slice = append(slice, slice2...)fmt.Println(slice)      //[100 200 300]fmt.Println(len(slice)) //3fmt.Println(cap(slice)) //5

追加切片时,切片后必须加"..."来解包切片,意思就是将一个切片的所有元素展开,这是因为切片是 [ ]int 类型,而 append 要求接收的参数是 int 类型,如果直接传入 slice2 会报错:

cannot use slice2 (variable of type []int) as int value in argument to append

4. 追加它自己

	var slice []intslice = append(slice, 1, 2)slice = append(slice, 3, 4, 5)slice = append(slice, slice...)fmt.Println(slice)  //[1 2 3 4 5 1 2 3 4 5]

4.2 是否扩容

在 go 中,append 函数在向切片(slice)追加元素时,只有在当前容量(cap)不足时才会触发扩容

4.2.1 不扩容

当切片的容量足够:len(slice) + 新增元素数 <= cap(slice) 时,不会扩容

eg1:容量足够时不扩容

	slice := make([]int, 2, 5)  //len=2, cap=5slice = append(slice, 3)  //len=3, cap=5

eg2:容量不足时扩容

	slice := []int{1, 2, 3}  //len=3, cap=3slice = append(slice, 5) //len=4, cap=6

不扩容的底层机制

每次调用 append 函数,必须先检测slice底层数组是否有足够的容量来保存新添加的元素。如果有足够空间的话,直接扩展slice(依然在原有的底层数组之上),将新添加的元素复制到新扩展的空间,并返回slice

底层实现(伪代码)

 func appendNoGrow(slice []T, elements ...T) []T {newLen := len(slice) + len(elements)if newLen <= cap(slice) {  // 容量足够newSlice := slice[:newLen]  // 扩展 lencopy(newSlice[len(slice):], elements)  // 追加数据return newSlice}// 否则触发扩容...
}

首先计算新的长度 newLen ,通过slice创建一个新的扩展的切片,再使用copy 函数将新的元素复制


4.2.2 扩容

当切片的容量不足:len(slice) + 新增元素数 > cap(slice) 时,就会扩容

扩容的底层机制

如果没有足够的增长空间的话,append 函数则会先分配一个足够大的slice用于保存新的结果,先将原切片复制到新的空间,然后添加元素,最后新的切片和原切片引用不同的底层数组

基本扩容规则:

  • 新容量(newCap)的计算
    • 若当前容量(oldCap)<1024,则二倍扩容,newCap = 2 * oldCap
    • 若当前容量 >= 1024, 则 newCap = 1.25 * oldCap
  • 内存对齐:最终容量会根据切片类型的大小(如 int、struct 等)向上取整到最近的内存对齐值(避免内存碎片)

append 语句示例:

	slice := []int{1, 2, 3}  //len=3, cap=3slice = append(slice, 5) //len=4, cap=6

扩容的具体步骤

  1. 计算新容量

    • 假设 oldCap = 3(当前容量),追加一个元素
    • 新容量 newCap = 3 * 2 = 6 (oldCap < 1024,二倍扩容)
  2. 分配新数组

    • 创建一个长度为newCap 的新底层数组
  3. 数据迁移

    • 将旧数组的元素复制到新数组
  4. 追加新元素

    • 在新数组的末尾添加新元素
  5. 更新切片

    • 新切片的 len 为 oldLen + 新增元素数,cap 为newCap

特殊案例

追加多个元素:一次性追加多个元素时,扩容会直接 计算总需求

	slice := []int{1, 2} //len=2, cap=2slice = append(slice, 3, 4, 5) //len=5, cap=6

新容量计算:

  • 需求容量:needCap = 2 + 3 = 5
  • 按规则,newCap = 2 * 2 = 4(但4 < 5),所以会继续扩容直到 newCap >=5。这里依据二倍扩容我们预期 newCap = 8,但实际上go 的扩容策略比二倍扩容更复杂,并且有着优化,所以最终 newCap 实际上是6

为什么对 slice 扩容不直接使用 append(slice, 1)
而是要 slice = append(slice, 1)
这是因为
通常我们并不知道append调用是否导致了内存的重新分配,因此我们也不能确认新的slice和原始的slice是否引用的是相同的底层数组空间。同样,我们不能确认在原先的slice上的操作是否会影响到新的slice。因此,通常是将append返回的结果直接赋值给输入的slice变量


总结

总的来说 slice 的底层还是比较重要,对于后续的学习和面试都必不可少


http://www.hkcw.cn/article/isUEWkeinX.shtml

相关文章

Fashion-MNIST LeNet训练

前面使用线性神经网络softmax 和 多层感知机进行图像分类&#xff0c;本次我们使用LeNet 卷积神经网络进行 训练&#xff0c;期望能捕捉到图像中的图像结构信息&#xff0c;提高识别精度&#xff1a; import torch import torchvision from torchvision import transforms f…

数据库系统概论(十)SQL 嵌套查询 超详细讲解(附带例题表格对比带你一步步掌握)

数据库系统概论&#xff08;十&#xff09;SQL 嵌套查询 超详细讲解&#xff08;附带例题表格对比带你一步步掌握&#xff09; 前言一、什么是嵌套查询&#xff1f;1. 基础组成&#xff1a;查询块2. 嵌套的两种常见位置&#xff08;1&#xff09;藏在 FROM 子句里&#xff08;当…

Azure 机器学习初学者指南

Azure 机器学习初学者指南 在我们的初学者指南中探索Azure机器学习&#xff0c;了解如何设置、部署模型以及在Azure生态系统中使用AutoML & ML Studio。Azure 机器学习 &#xff08;Azure ML&#xff09; 是一项全面的云服务&#xff0c;专为机器学习项目生命周期而设计&am…

使用win11圆角指针教程

一.准备文件 win11圆角指针下载链接&#xff1a;https://wwxh.lanzoum.com/iwsZH2xqmy0d 密码&#xff1a;em 二.开始安装 1.将下载的压缩包解压&#xff08;随便存哪&#xff0c;最后可以删掉&#xff09; 右键&#xff0c;点击“全部解压缩” 点击“提取” 2.安装 选…

day16 leetcode-hot100-30(链表9)

24. 两两交换链表中的节点 - 力扣&#xff08;LeetCode&#xff09; 1.模拟法 思路 模拟题目要求进行两两交换&#xff0c;但有一点需要注意&#xff0c;比如交换3与4后&#xff0c;1仍然指的是3&#xff0c;这是不正确的&#xff0c;所以1指针的next也需要修改&#xff0c;所…

C语言进阶--程序的编译(预处理动作)+链接

1.程序的翻译环境和执行环境 在ANSI C标准的任何一种实现中&#xff0c;存在两种不同的环境。 第一种是翻译环境&#xff1a;将源代码转换为可执行的机器指令&#xff08;0/1&#xff09;; 第二种是执行环境&#xff1a;用于实际执行代码。 2.详解编译链接 2.1翻译环境 程…

GCA解码大脑因果网络

格兰杰因果分析&#xff08;Granger Causality Analysis,GCA&#xff09; 是一种测量脑区之间有效性连接&#xff08;effective connectivity&#xff09;的成熟方法。利用多元线性回归分析一个时间序列的过去值是否能正确预测另一个时间序列的当前值&#xff0c;可以用来描述脑…

H5S 大华SDK带图报警类型及热成像报警支持

目前很多应用都希望报警带对应的图片&#xff0c;比如控制中心在弹报警框的时候需要有一张图片让人工更快的做出判断&#xff0c;下面介绍使用大华SDK 的带图报警功能。 大华SDK支持接入设备带图报警&#xff0c;并且支持热成像通道报警&#xff0c;设置订阅事件并吧协议端口设…

(javaSE)Java数组进阶:数组初始化 数组访问 数组中的jvm 空指针异常

数组的基础 什么是数组呢? 数组指的是一种容器,可以用来存储同种数据类型的多个值 数组的初始化 初始化&#xff1a;就是在内存中,为数组容器开辟空间,并将数据存入容器中的过程。 数组初始化的两种方式&#xff1a;静态初始化&#xff0c;动态初始化 数组的静态初始化 初始化…

Java数据结构——八大排序

排序 插⼊排序希尔排序直接选择排序堆排序冒泡排序快速排序归并排序计数排序 排序的概念 排序&#xff1a;就是将一串东西&#xff0c;按照要求进行排序&#xff0c;按照递增或递减排序起来 稳定性&#xff1a;就是比如排序中有两个相同的数&#xff0c;如果排序后&#xff0c…

【Linux】Linux文件系统详解

目录 Linux系统简介 Linux常见发行版&#xff1a; Linux/windows文件系统区别 Linux文件系统各个目录用途 Linux系统核心文件 系统核心配置文件 用户与环境配置文件 系统运行与日志文件 Linux文件名颜色含义 Linux文件关键信息解析 &#x1f525;个人主页 &#x1f52…

2023年6月6级第一套第一篇

虽然&#xff0c;不重要题干定位到主句信息了&#xff0c;往下走&#xff0c;看强调什么信息看最后一句&#xff0c;优先看主干信息&#xff0c;先找谓语然后找主语和宾语&#xff0c;也是和人有关&#xff0c;后面出现的名词信息是修饰部分&#xff0c;非主干信息不看 A选项&…

Langchaine4j 流式输出 (6)

Langchaine4j 流式输出 大模型的流式输出是指大模型在生成文本或其他类型的数据时&#xff0c;不是等到整个生成过程完成后再一次性 返回所有内容&#xff0c;而是生成一部分就立即发送一部分给用户或下游系统&#xff0c;以逐步、逐块的方式返回结果。 这样&#xff0c;用户…

代谢组数据分析(二十六):LC-MS/MS代谢组学和脂质组学数据的分析流程

禁止商业或二改转载,仅供自学使用,侵权必究,如需截取部分内容请后台联系作者! 文章目录 介绍加载R包依赖包安装包加载需要的R包数据下载以及转换mzML数据预处理代谢物注释LipidFinder过滤MultiABLER数据预处理过滤补缺失值对数变换数据标准化下游数据分析总结系统信息参考介…

常量指真,指针常量 ,

const int*p&#xff1b;//const int 值不能变 指向可以变 int *const p&#xff1b;//const p 指向不可以变 值能变

智能指针unique

什么是智能指针&#xff1a; 就像是一个自动管家 帮你管理内存 自动清理不需要的内存 防止内存泄漏 unique_ptr 的特点&#xff1a; 独占所有权&#xff1a;一个资源只能被一个 unique_ptr 管理 不能复制&#xff1a;只能移动 自动释放&#xff1a;当 unique_ptr 被销毁…

并发执行问题 下

这段例子 是让S3 在S2后面运行 写完数据 通知后 另一个进程 竞争使用资源 独占资源 shell解释器 科学语言才有并发语句语言 C语言没有 使用多线程和多进程实现并发运行

[JS逆向] 福建电子交易平台

博客配套代码发布于github&#xff1a;福建电子交易平台 相关知识点&#xff1a;[爬虫知识] 密码学&#xff1a;通往JS逆向路上必会的一环 相关爬虫专栏&#xff1a;JS逆向爬虫实战 爬虫知识点合集 爬虫实战案例 此案例目标为对福建省电子公共服务平台逆向&#xff0c;并爬…

Mask_RCNN 环境配置及训练

目录 一、Mask_RCNN代码及权重 1、源码下载 2、权重获取 二、环境配置 1、创建虚拟环境 2、安装必要的包 三、测试环境 1、使用coco 2、使用balloon 四、测试 1、使用coco 2、使用balloon 一、Mask_RCNN代码及权重 均从github获取&#xff0c;以下是相关链接&#…

72.编辑用户消息功能之前端实现

大体设想 我想实现的一个功能是在用户发出的消息下面有一个图标是编辑&#xff0c;按下那个图标之后&#xff0c;用户可以修改对应的那个消息&#xff0c;修改完成点击确认之后&#xff0c;用户下面对用的那个AI的回答可以重新生成 之前已经介绍了后端实现&#xff0c;这篇博…