[Go] slice切片详解

切片详解

切片的实现

Go 中的切片本质上是一个结构体,包含以下三个部分:

  • 指向底层数组的指针array):切片指向一个底层数组,数组中存储着切片的数据。
  • 切片的长度len):切片中当前元素的个数。
  • 切片的容量cap):底层数组的总容量,即底层数组能够存储的元素个数。
type slice struct {array unsafe.Pointerlen   intcap   int
}

切片的扩容

切片在添加元素(如append操作)时,如果切片的长度大于容量,那么会重新分配一个新的容量更大的底层数组来存储新切片

切片在初始化的时候长度等于容量,当向切片添加元素时切片的长度就会大于容量,此时就会为其分配一个新的底层数组

func main() {t := []int{1, 2, 3}fmt.Println(cap(t))fmt.Printf("%p\n", t)t = append(t, 1)fmt.Println(cap(t))fmt.Printf("%p\n", t)//3//0xc0000120a8//6//0xc00000c360
}

在该示例中,初始化切片的长度为3,容量也为3,切片的底层数组的地址为0xc0000120a8,当向其添加一个元素后,切片的容量为6,并指向了一个新的地址0xc00000c360

Tips:使用 fmt.Printf("%p\n", t) 获得的是切片指向的底层数组的地址与 fmt.Printf("%p\n", unsafe.Pointer(&t[0])) 的值相同,使用 fmt.Printf("%p\n", &t) 获得的是切片变量本身的地址

我们可以使用make()创建一个容量大于长度的切片,这样在向切片添加元素时,长度就不会超过容量,就不会分配新的数组

func main() {t := make([]int, 3, 5)fmt.Println(len(t))fmt.Println(cap(t))fmt.Printf("%p\n", t)t = append(t, 1)fmt.Println(cap(t))fmt.Printf("%p\n", t)//3//5//0xc00000c360//5//0xc00000c360
}

在该示例中,我们使用make([]int, 3, 5)创建了一个长度为3,容量为5的切片,随后我们向其添加了一个元素,长度为4没有超过容量,此时底层数组的地址与添加元素前的地址一致,并没有分配新的数组。

切片扩容的实现

在第一个示例中,当我们向一个容量为3的切片添加一个元素后,新分配的切片容量为6,这个是如何得出的呢?

切片扩容的代码为 growslice

func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice {oldLen := newLen - numif raceenabled {callerpc := getcallerpc()racereadrangepc(oldPtr, uintptr(oldLen*int(et.Size_)), callerpc, abi.FuncPCABIInternal(growslice))}if msanenabled {msanread(oldPtr, uintptr(oldLen*int(et.Size_)))}if asanenabled {asanread(oldPtr, uintptr(oldLen*int(et.Size_)))}if newLen < 0 {panic(errorString("growslice: len out of range"))}if et.Size_ == 0 {// append should not create a slice with nil pointer but non-zero len.// We assume that append doesn't need to preserve oldPtr in this case.return slice{unsafe.Pointer(&zerobase), newLen, newLen}}newcap := nextslicecap(newLen, oldCap)var overflow boolvar lenmem, newlenmem, capmem uintptr// Specialize for common values of et.Size.// For 1 we don't need any division/multiplication.// For goarch.PtrSize, compiler will optimize division/multiplication into a shift by a constant.// For powers of 2, use a variable shift.noscan := !et.Pointers()switch {case et.Size_ == 1:lenmem = uintptr(oldLen)newlenmem = uintptr(newLen)capmem = roundupsize(uintptr(newcap), noscan)overflow = uintptr(newcap) > maxAllocnewcap = int(capmem)case et.Size_ == goarch.PtrSize:lenmem = uintptr(oldLen) * goarch.PtrSizenewlenmem = uintptr(newLen) * goarch.PtrSizecapmem = roundupsize(uintptr(newcap)*goarch.PtrSize, noscan)overflow = uintptr(newcap) > maxAlloc/goarch.PtrSizenewcap = int(capmem / goarch.PtrSize)case isPowerOfTwo(et.Size_):var shift uintptrif goarch.PtrSize == 8 {// Mask shift for better code generation.shift = uintptr(sys.TrailingZeros64(uint64(et.Size_))) & 63} else {shift = uintptr(sys.TrailingZeros32(uint32(et.Size_))) & 31}lenmem = uintptr(oldLen) << shiftnewlenmem = uintptr(newLen) << shiftcapmem = roundupsize(uintptr(newcap)<<shift, noscan)overflow = uintptr(newcap) > (maxAlloc >> shift)newcap = int(capmem >> shift)capmem = uintptr(newcap) << shiftdefault:lenmem = uintptr(oldLen) * et.Size_newlenmem = uintptr(newLen) * et.Size_capmem, overflow = math.MulUintptr(et.Size_, uintptr(newcap))capmem = roundupsize(capmem, noscan)newcap = int(capmem / et.Size_)capmem = uintptr(newcap) * et.Size_}// The check of overflow in addition to capmem > maxAlloc is needed// to prevent an overflow which can be used to trigger a segfault// on 32bit architectures with this example program://// type T [1<<27 + 1]int64//// var d T// var s []T//// func main() {//   s = append(s, d, d, d, d)//   print(len(s), "\n")// }if overflow || capmem > maxAlloc {panic(errorString("growslice: len out of range"))}var p unsafe.Pointerif !et.Pointers() {p = mallocgc(capmem, nil, false)// The append() that calls growslice is going to overwrite from oldLen to newLen.// Only clear the part that will not be overwritten.// The reflect_growslice() that calls growslice will manually clear// the region not cleared here.memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)} else {// Note: can't use rawmem (which avoids zeroing of memory), because then GC can scan uninitialized memory.p = mallocgc(capmem, et, true)if lenmem > 0 && writeBarrier.enabled {// Only shade the pointers in oldPtr since we know the destination slice p// only contains nil pointers because it has been cleared during alloc.//// It's safe to pass a type to this function as an optimization because// from and to only ever refer to memory representing whole values of// type et. See the comment on bulkBarrierPreWrite.bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(oldPtr), lenmem-et.Size_+et.PtrBytes, et)}}memmove(p, oldPtr, lenmem)return slice{p, newLen, newcap}
}
  • oldPtr unsafe.Pointer:指向当前切片的底层数组的指针。

  • newLen int:扩展后的切片的目标长度。

  • oldCap int:当前切片的容量。

  • num int:额外需要的空间量,通常用于处理切片扩容时的新数据量。

  • et *_type:元素的类型,用来确定切片元素的大小和是否是指针类型(如 int*struct)。

首先处理一些边界条件,随后调用nextslicecap方法计算新切片的容量,具体细节如下

func nextslicecap(newLen, oldCap int) int {newcap := oldCapdoublecap := newcap + newcapif newLen > doublecap {return newLen}const threshold = 256if oldCap < threshold {return doublecap}for {// 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) >> 2// We need to check `newcap >= newLen` and whether `newcap` overflowed.// newLen is guaranteed to be larger than zero, hence// when newcap overflows then `uint(newcap) > uint(newLen)`.// This allows to check for both with the same comparison.if uint(newcap) >= uint(newLen) {break}}// Set newcap to the requested cap when// the newcap calculation overflowed.if newcap <= 0 {return newLen}return newcap
}

注意,以上计算出的容量并不是新切片最终的容量,在接下来的33行的switch中,还会进行一些内存管理的操作,因为内存都是成块分配的,所以实际分配的容量可能会由于内存对齐等原因大于nextslicecap计算出的newcap;

最后使用mallogc分配实际的内存大小capmem

切片传参

go语言中只有值传递,没有引用传递,也就是说任何变量在传递时都会复制一份副本

func main() {t := []int{1, 2, 3}fmt.Printf("slice array address: %p\n", t)fmt.Printf("slice address: %p\n", &t)test(t)//slice array address: 0xc0000120a8//slice address: 0xc000008030//slice array address(in function): 0xc0000120a8//slice address(in function): 0xc000008060
}func test(t []int) {fmt.Printf("slice array address(in function): %p\n", t)fmt.Printf("slice address(in function): %p\n", &t)
}

以上示例可以看到函数内外的切片地址并不相同,但是他们指向的底层数组是相同的,这符合值传递的特征。

这里我们在结合之前提到的切片扩容进行分析

func main() {t := []int{1, 2, 3}fmt.Printf("%p\n", t)test(t)fmt.Printf("%p\n", t)//0xc0000120a8//0xc0000120a8//0xc00000c360//0xc0000120a8
}func test(t []int) {fmt.Printf("%p\n", t)t = append(t, 1)fmt.Printf("%p\n", t)
}

以上示例中,方法中接收的切片在开始时与原始切片指向同一个数组,但是在添加一个元素后,切片的容量超过了长度,此时为方法内的切片分配了一个新的底层数组,但是由于是值传递,两个切片本身指向不同的地址,所以方法外的切片的数组并没有改变。

func main() {t := make([]int, 3, 10)fmt.Printf("%p\n", t)test(t)fmt.Printf("%p\n", t)//0xc000014140//0xc000014140//0xc000014140//0xc000014140
}func test(t []int) {fmt.Printf("%p\n", t)t = append(t, 1)fmt.Printf("%p\n", t)
}

以上示例中切片的容量足够,所以不会分配新的数组,但是要注意的是虽然没有分配新的数组,但是切片的长度发生了改变。

func main() {t := make([]int, 3, 10)test(t)fmt.Println(t)//[0 0 0 1]//[0 0 0]
}func test(t []int) {t = append(t, 1)fmt.Println(t)
}

以上示例可以看出,在函数内外输出的切片并不一致,这是因为值传递,函数内的切片的长度发生了改变并不会改变函数外的切片长度

func main() {t := make([]int, 3, 10)test(t)fmt.Println(t)newt := t[0:4]fmt.Println(newt)//[0 0 0 1]//[0 0 0]//[0 0 0 1]
}func test(t []int) {t = append(t, 1)fmt.Println(t)
}

这里我们通过将t赋值给一个新的切片来取出它的第4个元素可以看到与函数内的修改一致,证明他们确实是同一个底层数组,注意这里不能通过下标的方式直接取值会报越界错误。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/61118.shtml

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

相关文章

ros2键盘实现车辆: 简单的油门_刹车_挡位_前后左右移动控制

参考: ROS python 实现键盘控制 底盘移动 https://blog.csdn.net/u011326325/article/details/131609340游戏手柄控制 1.背景与需求 1.之前实现过 键盘控制 底盘移动的程序, 底盘是线速度控制, 效果还不错. 2.新的底盘 只支持油门控制, 使用线速度控制问题比较多, 和底盘适配…

DICOM医学影像应用篇——窗宽窗位概念、原理及实现详解

目录 窗宽窗位调整&#xff08;Windowing&#xff09;在DICOM医学影像中的应用 窗宽窗位的基本概念 窗宽&#xff08;Window Width, WW&#xff09; 窗位&#xff08;Window Level, WL&#xff09; 窗宽窗位调整的基本原理 映射逻辑 数学公式 窗宽窗位调整的C实现 代码…

天锐绿盾加密软件与Ping32联合打造企业级安全保护系统,确保敏感数据防泄密与加密管理

随着信息技术的飞速发展&#xff0c;企业在日常经营过程中产生和处理的大量敏感数据&#xff0c;面临着越来越复杂的安全威胁。尤其是在金融、医疗、法律等领域&#xff0c;数据泄漏不仅会造成企业巨大的经济损失&#xff0c;还可能破坏企业的信誉和客户信任。因此&#xff0c;…

HarmonyOS:@Provide装饰器和@Consume装饰器:与后代组件双向同步

一、前言 Provide和Consume&#xff0c;应用于与后代组件的双向数据同步&#xff0c;应用于状态数据在多个层级之间传递的场景。不同于上文提到的父子组件之间通过命名参数机制传递&#xff0c;Provide和Consume摆脱参数传递机制的束缚&#xff0c;实现跨层级传递。 其中Provi…

【Spring MVC】如何运用应用分层思想实现简单图书管理系统前后端交互工作

前言 &#x1f31f;&#x1f31f;本期讲解关于SpringMVC的编程思想之应用分层~~~ &#x1f308;感兴趣的小伙伴看一看小编主页&#xff1a;GGBondlctrl-CSDN博客 &#x1f525; 你的点赞就是小编不断更新的最大动力 &#x1f386;那…

【Linux】项目自动化构建工具-make/Makefile

【Linux】项目自动化构建工具-make/Makefile make 和 makefile 的概念如何清理项目推导过程Linux第⼀个小程序−倒计时 &#x1f30f;个人博客主页&#xff1a;个人主页 make 和 makefile 的概念 make是一个命令工具&#xff0c;是一个解释makefile中指令的命令工具&#xf…

arcgis for js点击聚合要素查询其包含的所有要素

功能说明 上一篇讲了实现聚合效果, 但是点击聚合效果无法获取到该聚合点包含的所有点信息 这一篇是对如何实现该功能的案例 实现 各属性说明需要自行去官网查阅 官网案例 聚合API 没空说废话了, 加班到12点,得休息了, 直接运行代码看效果就行, 相关重点和注意事项都在代码注…

【计算机视觉】图像基本操作

1. 数字图像表示 一幅尺寸为MN的图像可以用矩阵表示&#xff0c;每个矩阵元素代表一个像素&#xff0c;元素的值代表这个位置图像的亮度&#xff1b;其中&#xff0c;彩色图像使用3维矩阵MN3表示&#xff1b;对于图像显示来说&#xff0c;一般使用无符号8位整数来表示图像亮度&…

javaweb-day03-前端零碎

1.Ajax &#xff08;1&#xff09;概述 &#xff08;2&#xff09;原生Ajax-繁琐&#xff0c;现已基本弃用 2.Ajax-Axios &#xff08;2&#xff09;案例 3.前端工程化 3.1 基础 3.2 vue项目 &#xff08;1&#xff09;项目目录结构 &#xff08;2&#xff09;主要开发…

论文阅读:A Software Platform for Manipulating theCamera Imaging Pipeline

论文代码开源链接&#xff1a; A Software Platform for Manipulating the Camera Imaging Pipelinehttps://karaimer.github.io/camera-pipeline/摘要&#xff1a;论文提出了一个Pipline软件平台&#xff0c;可以方便地访问相机成像Pipline的每个阶段。该软件允许修改单个模块…

Python毕业设计选题:基于django+vue的智能停车系统的设计与实现

开发语言&#xff1a;Python框架&#xff1a;djangoPython版本&#xff1a;python3.7.7数据库&#xff1a;mysql 5.7数据库工具&#xff1a;Navicat11开发软件&#xff1a;PyCharm 系统展示 管理员登录 管理员功能界面 车主管理 车辆信息管理 车位信息管理 车位类型管理 系统…

使用phpStudy小皮面板模拟后端服务器,搭建H5网站运行生产环境

一.下载安装小皮 小皮面板官网下载网址&#xff1a;小皮面板(phpstudy) - 让天下没有难配的服务器环境&#xff01; 安装说明&#xff08;特别注意&#xff09; 1. 安装路径不能包含“中文”或者“空格”&#xff0c;否则会报错&#xff08;例如错误提示&#xff1a;Cant cha…

【jmeter】服务器使用jmeter压力测试(从安装到简单压测示例)

一、服务器上安装jmeter 1、官方下载地址&#xff0c;https://jmeter.apache.org/download_jmeter.cgi 2、服务器上用wget下载 # 更新系统 sudo yum update -y# 安装 wget 以便下载 JMeter sudo yum install wget -y# 下载 JMeter 压缩包&#xff08;使用 JMeter 官方网站的最…

【大数据学习 | Spark-Core】详解Spark的Shuffle阶段

1. shuffle前言 对spark任务划分阶段&#xff0c;遇到宽依赖会断开&#xff0c;所以在stage 与 stage 之间会产生shuffle&#xff0c;大多数Spark作业的性能主要就是消耗在了shuffle环节&#xff0c;因为该环节包含了大量的磁盘IO、序列化、网络数据传输等操作。 负责shuffle…

Flink高可用配置(HA)

从Flink架构中我们可以看到,JobManager这个组件非常重要,是中心协调器,负责任务调度和资源管理。默认情况下,每个Flink集群只有一个JobManager实例。这会产生单点故障(SPOF):如果JobManager崩溃,则无法提交新程序,正在运行的程序也会失败。通过JobManager的高可用性,…

Meta 发布Sapiens人类视觉模型,2D 姿势估计、人体分割、深度估计

meta提出了 Sapiens&#xff0c;人类基础视觉模型。这是一个以人为中心的视觉任务的模型。包括&#xff1a; 2D 姿势估计、人体部位分割、深度估计和表面法线预测。 此模型本身支持 1K 高分辨率推理&#xff0c;Sapiens在超过 3 亿张人类图像上预训练的模型进行微调&#xff0c…

NLP论文速读(EMNLP2024)|多风格可控生成的动态多奖励权重

论文速读|Dynamic Multi-Reward Weighting for Multi-Style Controllable Generation 论文信息&#xff1a; 简介&#xff1a; 本文探讨了文本风格在沟通中的重要性&#xff0c;指出文本风格传达了除原始语义内容之外的多种信息&#xff0c;如人际关系动态&#xff08;例如正式…

鸿蒙中的Image组件如何引用网络图片

1.引用网络图片资源 引入网络图片需要申请权限ohos.permission.INTERNET&#xff0c;此时&#xff0c;Image组件的src参数为网络图片的链接&#xff0c;为了成功加载网络图片&#xff0c;您需要在module.json5文件中申请网络访问权限 注意&#xff1a;实际可用的时候&#xff0…

七天掌握SQL--->第七天:项目实践与总结

一、项目实践 1.1 项目背景 假设我们正在开发一个名为“在线图书管理系统”的项目。该项目旨在帮助图书馆管理员管理图书的借阅、归还、库存等操作&#xff0c;同时为读者提供一个便捷的图书查询和借阅平台。 1.2 数据库设计 1.2.1 需求分析 根据项目的需求&#xff0c;我…

React Hooks中use的细节

文档 useState useState如果是以函数作为参数&#xff0c;那要求是一个纯函数&#xff0c;不接受任何参数&#xff0c;同时需要一个任意类型的返回值作为初始值。 useState可以传入任何类型的参数作为初始值&#xff0c;当以一个函数作为参数进行传入的时候需要注意&#xff…