golang -- slice 底层逻辑

news2025/6/5 8:41:26

目录

  • 一、前言
  • 二、结构
  • 三、创建
    • 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.Pointer
	len   int
	cap   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] = 100
	slice[1] = 200
	slice[2] = 300

	fmt.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))  //5
	fmt.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=9
	slice2 := array[:2]  //[0 1]  len=2  cap=10
	slice3 := 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)) //1
	fmt.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)) //3
	fmt.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)) //3
	fmt.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 []int
	slice = 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=5
	slice = append(slice, 3)  //len=3, cap=5

eg2:容量不足时扩容

	slice := []int{1, 2, 3}  //len=3, cap=3
	slice = 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]  // 扩展 len
        copy(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=3
	slice = 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=2
	slice = 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.coloradmin.cn/o/2397784.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SOC-ESP32S3部分:26-物联网MQTT连云

飞书文档https://x509p6c8to.feishu.cn/wiki/IGCawAgqFibop7kO83KcsDFBnNb ESP-MQTT 是 MQTT 协议客户端的实现&#xff0c;MQTT 是一种基于发布/订阅模式的轻量级消息传输协议。ESP-MQTT 当前支持 MQTT v5.0。 特性 支持基于 TCP 的 MQTT、基于 Mbed TLS 的 SSL、基于 WebSo…

制造业的未来图景:超自动化与劳动力转型的双重革命

市场现状&#xff1a;传统制造业的转型阵痛 当前全球制造业正站在历史性变革的十字路口。埃森哲对552位工厂经理的全球调研显示&#xff0c;60%的受访者将劳动力转型视为首要战略任务​​&#xff0c;而63%的工厂正在加速部署自动化技术[1]。超过​75%的工厂经理​​认为&…

【Unity】相机 Cameras

1 前言 主要介绍官方文档中相机模块的内容。 关于“9动态分辨率”&#xff0c;这部分很多API文档只是提了一下&#xff0c;具体细节还需要自己深入API才行。 2 摄像机介绍 Unity 场景在三维空间中表示游戏对象。由于观察者的屏幕是二维屏幕&#xff0c;Unity 需要捕捉视图并将…

如何在 Solana 上发币,并创建初始流动性让项目真正“动”起来?

在 Solana 上发行代币如今已不再是技术门槛&#xff0c;而是市场策略和执行效率的较量。如果你只是简单发了一个代币&#xff0c;却没为它建立流动性和市场机制&#xff0c;那么它就只是一个“死币”。 本文将带你一步步理解&#xff0c;如何从发币到建立流动性池&#xff0c;…

核心机制:滑动窗口

TCP 协议 1.确认应答 可靠传输的核心机制 2.超时重传 可靠传输的核心机制 3.连接管理 TCP/网络 最高的面试题 三次握手,建立连接(必须是 三次) 四次挥手,断开连接(可能是 三次) 核心机制四:滑动窗口 算法中的"滑动窗口" 出自 TCP 前面的三个…

苹果电脑深度清理,让老旧Mac重焕新生

在日常使用苹果电脑的过程中&#xff0c;随着时间推移&#xff0c;系统内会积累大量冗余数据&#xff0c;导致电脑运行速度变慢、磁盘空间紧张。想要让设备恢复流畅&#xff0c;苹果电脑深度清理必不可少。那么&#xff0c;如何进行苹果电脑深度清理呢&#xff1f;接下来为你详…

微服务面试(分布式事务、注册中心、远程调用、服务保护)

1.分布式事务 分布式事务&#xff0c;就是指不是在单个服务或单个数据库架构下&#xff0c;产生的事务&#xff0c;例如&#xff1a; 跨数据源的分布式事务跨服务的分布式事务综合情况 我们之前解决分布式事务问题是直接使用Seata框架的AT模式&#xff0c;但是解决分布式事务…

高性能MYSQL(三):性能剖析

一、性能剖析概述 &#xff08;一&#xff09;关于性能优化 1.什么是性能&#xff1f; 我们将性能定义为完成某件任务所需要的时间度量&#xff0c;换句话说&#xff0c;性能即响应时间&#xff0c;这是一个非常重要的原则。 我们通过任务和时间而不是资源来测量性能。数据…

mysql(十四)

目录 多表查询 1.准备工作 2--创建表格 3--插入数据 2.笛卡尔积查询 3.内连接查询 1--隐式内连接 格式 查询 2--显示内连接&#xff08;Inner join .. on &#xff09; 格式 查询 4.外连接查询 1--左外连接查询&#xff08;LEFT OUTER JOIN .. ON &#xff09; 格式 查询 2-- 右…

工业物联网中的事件驱动采样架构及优化

论文标题 Event-Based Sampling Architecture and Optimization for Industrial Internet of Things 工业物联网中的事件驱动采样架构及优化 作者信息 Tejas Thosani Process Control Systems, Micron Technology Inc., Manassas, USA tthosanimicron.com Andres Prado Esp…

基于 HT for Web 的轻量化 3D 数字孪生数据中心解决方案

一、技术架构&#xff1a;HT for Web 的核心能力 图扑软件自主研发的 HT for Web 是基于 HTML5 的 2D/3D 可视化引擎&#xff0c;核心技术特性包括&#xff1a; 跨平台渲染&#xff1a;采用 WebGL 技术&#xff0c;支持 PC、移动端浏览器直接访问&#xff0c;兼容主流操作系统…

JavaScript 性能优化:从入门到实战

在当今快节奏的互联网时代&#xff0c;用户对网页和应用的加载速度与响应性能要求越来越高。JavaScript 作为网页交互的核心语言&#xff0c;其性能表现直接影响用户体验。本文将用简单易懂的语言&#xff0c;带你了解 JavaScript 性能优化的实用技巧&#xff0c;帮助你的代码跑…

启动metastore时报错MetaException(message:Version information not found in metastore

把hdfs清空重新安装了一下&#xff0c;hive的mysql元数据库删除掉之后重建之后一直启动报错 metastore.RetryingHMSHandler (RetryingHMSHandler.java:<init>(83)) - HMSHandler Fatal error: MetaException(message:Version information not found in metastore.) 后来…

MyBatisPlus(1):快速入门

我们知道&#xff0c;MyBatis是一个优秀的操作数据库的持久层框架&#xff08;优秀持久层框架——MyBatis&#xff09;&#xff0c;其基于底层的JDBC进行高度封装&#xff0c;极大的简化了开发。但是对于单表操作而言&#xff0c;我们需要重复地编写简单的CRUD语句。这其实是不…

京东热点缓存探测系统JDhotkey架构剖析

热点探测使用场景 MySQL 中被频繁访问的数据 &#xff0c;如热门商品的主键 IdRedis 缓存中被密集访问的 Key&#xff0c;如热门商品的详情需要 get goods$Id恶意攻击或机器人爬虫的请求信息&#xff0c;如特定标识的 userId、机器 IP频繁被访问的接口地址&#xff0c;如获取用…

【Elasticsearch】ILM(Index Lifecycle Management)策略详解

ILM&#xff08;Index Lifecycle Management&#xff09;策略详解 1.什么是 ILM 策略&#xff1f;2.ILM 解决的核心业务问题3.ILM 生命周期阶段3.1 Hot&#xff08;热阶段&#xff09;3.2 Warm&#xff08;温阶段&#xff09;3.3 Cold&#xff08;冷阶段&#xff09;3.4 Delete…

linux 后记

Linux Server 下载一个Server的版本&#xff0c;就是那种只有命令行的 学会这个就可以去租一个aliyun服务器&#xff0c;挺便宜的 如果在aliyun买服务器的话就不用管镜像源 但是如果是自己的虚拟机就必须设置镜像源&#xff0c;上网搜索阿里的镜像源&#xff0c;然后手动输入&…

【笔记】在 MSYS2 MINGW64 环境中安装构建工具链(CMake、GCC、Make)

&#x1f4dd; 在 MSYS2 MINGW64 环境中安装构建工具链&#xff08;CMake、GCC、Make&#xff09; ✅ 目标说明 记录在 MSYS2 的 MINGW64 工具链环境中&#xff0c;成功安装用于 C/C 构建的常用开发工具。 包括&#xff1a; GCC 编译器Make 构建系统CMake 跨平台构建工具基础开…

PyTorch -TensorBoard的使用 (一)

设置环境 新建python文件 .py 安装Tensorboard 在终端进行安装 显示安装成功 两个logs&#xff0c;出现这种情况怎么解决 所有的logs文件删掉delete&#xff0c;重新运行 add_image 不满足要求 Opencv-numpy 安装Opencv add_image 用法示例 &#xff08;500&#xff0c;375&am…

Redis最佳实践——性能优化技巧之数据结构选择

Redis在电商应用中的数据结构选择与性能优化技巧 一、电商核心场景与数据结构选型矩阵 应用场景推荐数据结构内存占用读写复杂度典型操作商品详情缓存Hash低O(1)HGETALL, HMSET购物车管理Hash中O(1)HINCRBY, HDEL用户会话管理Hash低O(1)HSETEX, HGET商品分类目录Sorted Set高O…