小白学go基础06-了解切片实现原理并高效使用

news2024/11/27 5:23:15

slice,中文多译为切片,是Go语言在数组之上提供的一个重要的抽象数据类型。在Go语言中,对于绝大多数需要使用数组的场合,切片实现了完美替代。并且和数组相比,切片提供了更灵活、更高效的数据序列访问接口。

切片究竟是什么?

在对切片一探究竟之前,我们先来简单了解一下Go语言中的数组。

Go语言数组是一个固定长度的、容纳同构类型元素的连续序列,因此Go数组类型具有两个属性:元素类型和数组长度

这两个属性都相同的数组类型是等价的。比如以下变量
a、b、c对应的数组类型是三个不同的数组类型:

var a [8]int
var b [8]byte
var c [9]int

数组 a、b对应的数组类型长度属性相同,但元素类型不同(一个是int,另一个是byte);数组 a、c对应的数组类型的元素类型相同,都是int,但数组类型的长度不同(一个是8,另一个是9)。

Go数组是值语义的,这意味着一个数组变量表示的是整个数组,这点与C语言完全不同。在C语言中,数组变量可视为指向数组第一个元素的指针。而在Go语言中传递数组是纯粹的值拷贝,对于元素类型长度较大或元素个数较多的数组,如果直接以数组类型参数传递到函数中会有不小的性能损耗。这时很多人会使用数组指针类型来定义函数参数,然后
将数组地址传进函数,这样做的确可以避免性能损耗,但这是C语言的惯用法,在Go语言中,更地道的方式是使用切片。

切片之于数组就像是文件描述符之于文件。在Go语言中,数组更多是“退居幕后”,承担的是底层存储空间的角色;而切片则走向“前台”,为底层的存储(数组)打开了一个访问的“窗口”。

在这里插入图片描述
因此,我们可以称切片是数组的“描述符”。切片之所以能在函数参数传递时避免较大性能损耗,是因为它是“描述符”的特性,切片这个描述符是固定大小的,无论底层的数组元素类型有多大,切片打开的窗口有多长。

下面是切片在Go运行时(runtime)层面的内部表示:

//$GOROOT/src/runtime/slice.go
type slice struct {
 array unsafe.Pointer
 len int
 cap int
}

我们看到每个切片包含以下三个字段。

● array:指向下层数组某元素的指针,该元素也是切片的起始元素。

● len:切片的长度,即切片中当前元素的个数。

● cap:切片的最大容量,cap >= len

在运行时中,每个切片变量都是一个runtime.slice结构体类型的实例,我们可以用下面的语句创建一个切片实例s

s := make([]byte, 5)

下图展示了切片s在运行时层面的内部表示:

在这里插入图片描述
我们看到通过上述语句创建的切片,编译器会自动为切片建立一个底层数组,如果没有在make中指定cap参数,那么cap = len,即编译器建立的数组长度为len

我们可以通过语法u[low: high]创建对已存在数组进行操作的切片,这被称为数组的切片化(slicing):

u := [10]byte{11, 12, 13, 14, 15, 16, 17, 18, 19, 20}
s := u[3:7]

下图展示了切片s的内部。

在这里插入图片描述
还可以通过语法s[low: high]基于已有切片创建新的切片,这被称为切片的reslicing。

如下图所示:新创建的切片与原切片同样是共享底层数组的,并且通过新切片对数组的修改也会反映到原切片中。

在这里插入图片描述

当切片作为函数参数传递给函数时,实际传递的是切片的内部表示,也就是上面的runtime.slice结构体实例,因此无论切片描述的底层数组有多大,切片作为参数传递带来的性能损耗都是很小且恒定的,甚至小到可以忽略不计,这就是函数在参数中多使用切片而不用数组指针的原因之一。

切片的高级特性:动态扩容

如果仅仅是提供通过下标值来操作元素的类数组操作接口,那么切片也不会在Go中占据重要的位置。Go切片还支持一个重要的高级特性:动态扩容。

零值切片也可以通过append预定义函数进行元素赋
值操作:

var s []byte // s被赋予零值nil
s = append(s, 1)

由于初值为零值,s这个描述符并没有绑定对应的底层数组。而经过append操作后,s显然已经绑定了属于它的底层数组。为了方便查看切片是如何动态扩容的,我们打印出每次append操作后切片s的len和cap值:

// chapter3/sources/slice_append.go
var s []int // s被赋予零值nil
s = append(s, 11)
fmt.Println(len(s), cap(s)) //1 1
s = append(s, 12)
fmt.Println(len(s), cap(s)) //2 2
s = append(s, 13)
fmt.Println(len(s), cap(s)) //3 4
s = append(s, 14)
fmt.Println(len(s), cap(s)) //4 4
s = append(s, 15)
fmt.Println(len(s), cap(s)) //5 8

我们看到切片s的len值是线性增长的,但cap值却呈现出不规则的变化。通过下图我们更容易看清楚多次append操作究竟是如何让切片进行动态扩容的。

在这里插入图片描述
我们看到append会根据切片对底层数组容量的需求对底层数组进行动态调整。

尽量使用cap参数创建切片

append操作是一件利器,它让切片类型部分满足了“零值可用”的理念。但从append的原理中我们也能看到重新分配底层数组并复制元素的操作代价还是挺大的,尤其是当元素较多的情况下。那么如何减少或避免为过多内存分配和复制付出的代价呢?

一种有效的方法是根据切片的使用场景对切片的容量规模进行预估,并在创建新切片时将预估出的切片容量数据以cap参数的形式传递给内置函数make:

s := make([]T, len, cap)

下面是一个使用cap参数和不使用cap参数的切片的性能基准测试:

const sliceSize = 10000
func BenchmarkSliceInitWithoutCap(b *testing.B) {
	for n := 0; n < b.N; n++ {
		sl := make([]int, 0)
			for i := 0; i < sliceSize; i++ {
				sl = append(sl, i)
		}
	}
}
func BenchmarkSliceInitWithCap(b *testing.B) {
	for n := 0; n < b.N; n++ {
		sl := make([]int, 0, sliceSize)
			for i := 0; i < sliceSize; i++ {
				sl = append(sl, i)
			}
		}
}

下面是性能基本测试运行的结果(Go 1.12.7;MacBook Pro:8核i5,16GB内存):

$go test -benchmem -bench=. slice_benchmark_test.go
goos: darwin
goarch: amd64
BenchmarkSliceInitWithoutCap-8 50000 36484 ns/op 386297 B/op 20 allocs/op
BenchmarkSliceInitWithCap-8 200000 9250 ns/op 81920 B/op 1 allocs/op
PASS
ok command-line-arguments 4.163s

由结果可知,使用带cap参数创建的切片进行append操作的平均性能(9250ns)是不带cap参数的切片(36 484ns)的4倍左右,并且每操作平均仅需一次内存分配。

切片是Go语言提供的重要数据类型,也是Gopher日常编码中最常使用的类型之一。切 片是数组的描述符,在大多数场合替代了数组,并减少了数组指针作为函数参数的使用。

append在切片上的运用让切片类型部分支持了“零值可用”的理念,并且append对切 片的动态扩容将Gopher从手工管理底层存储的工作中解放了出来。在可以预估出元素容量的前提下,使用cap参数创建切片可以提升append的平均操作性 能,减少或消除因动态扩容带来的性能损耗。

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

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

相关文章

JavaScript-----函数

目录 前言&#xff1a; JavaScript函数 1. 定义函数 构造函数 2. 调用函数 函数的自执行 3. 函数的参数 4. 函数返回值 5. 作用域 6. 匿名函数 7. this指向性问题&#xff08;重点&#xff09; 7.1 this的性质 7.2 call的用法 7.3 apply的用法 7.4 bind的用法&a…

KMP超高效匹配算法

简介&#xff1a; KMP算法是一种改进的字符串匹配算法&#xff0c;其中&#xff0c;KMP算法的运用核心是利用匹配失败后的信息&#xff0c;最大进度的减少模式串与目标串的匹配次数以达到快速匹配的效果。算法与暴力求解的改进在于每当一趟匹配过程中出现的字符比较不相等时&am…

无涯教程-JavaScript - NOW函数

描述 NOW函数返回当前日期和时间的序列号。 语法 NOW ()争论 NOW函数语法没有参数。 Notes 如果在输入功能之前单元格格式为"常规",则Excel会更改单元格格式,使其与您的区域设置的日期和时间格式匹配。您可以使用功能区"主页"options卡"数字&quo…

Java学习笔记——34多线程01

多线程 实现多线程进程和线程的区别多线程的实现方式方式一&#xff1a;继承Thread类设置线程名称线程调度线程控制线程生命周期 方式二&#xff1a;实现Runnable接口 实现多线程 进程和线程的区别 进程&#xff1a;是正在运行的程序 是系统进行资源分配和调用的独立单位每一…

文章预览 安防监控/视频存储/视频汇聚平台EasyCVR播放优化小tips

视频云存储/安防监控EasyCVR视频汇聚平台基于云边端智能协同&#xff0c;可实现视频监控直播、视频轮播、视频录像、云存储、回放与检索、智能告警、服务器集群、语音对讲、云台控制、电子地图、H.265自动转码H.264、平台级联等。为了便于用户二次开发、调用与集成&#xff0c;…

IG 自动回复:提供无间断客户互动体验

图片来源于&#xff1a;SaleSmartly官网 2023&#xff0c;Instagram拥有将近13亿的月活跃用户&#xff0c;在香港拥有超过400万活跃用户。 Instagram 以图片、长短影片、直播等高互动性的互动方式&#xff0c;吸引了广大的年轻族群&#xff0c;34岁以下的用户即占比了将近全球整…

Day55|动态规划part16:583. 两个字符串的删除操作、72. 编辑距离、编辑距离总结篇

583. 两个字符串的删除操作 leetcode链接&#xff1a;力扣题目链接 视频链接&#xff1a;&#xff1a;LeetCode&#xff1a;583.两个字符串的删除操 给定两个单词 word1 和 word2 &#xff0c;返回使得 word1 和 word2 相同所需的最小步数。每步 可以删除任意一个字符串中的…

两个路由器如何连接设置的方法攻略

一、前言 随着智能家居时代来临&#xff0c;家里的网络部署需求开始复杂起来。往往一个路由器已经不能满足需求或者不利于拓展。两个路由器连接最常见的情况是家中已有一个路由器&#xff0c;并且已经通过这个路由器来正常上网。现在是因某些原因想在不改变已经在用的路由器的设…

mysql 字段用了关键字, 无法插入更新数据

1. 表字段用了关键字 explain 2. sql语句中用包裹住关键字 (注意不是单引号) <if test"explain ! null">explain,</if>

CRMEB多端多语言系统文件上传0Day代审历程

Git仓库&#xff1a; https://github.com/crmeb/CRMEB简介&#xff1a; 两天攻防中&#xff0c;某政局子公司官网后台采用的CRMEB开源商城CMS&#xff0c;挺奇葩&#xff0c;别问怎么总让我碰到这种东西&#xff0c;我也不知道&#xff0c;主打的就是一个魔幻、抽象。最后通过…

基于环形队列的生产消费模型

目录 一、信号量 1.提出问题 2.信号量的概念 3.信号量的基本操作 &#xff08;1&#xff09;PV操作 &#xff08;2&#xff09;信号量的使用 二、基于环形队列的生产消费模型 1.环形队列 &#xff08;1&#xff09;复习 &#xff08;2&#xff09;现象 &#xff08;…

酷雷曼第二期无人机技能培训圆满举办

第2期无人机技能培训 2023年8月24日-8月25日&#xff0c;第二期酷雷曼无人机技能培训及执照考试在北京圆满举办&#xff0c;来自五湖四海、全国各地的合作商千里相聚&#xff0c;培训现场热闹融洽&#xff0c;再续精彩盛况。 随着《无人驾驶航空器飞行管理暂行条例》正式发布…

Mysql中group by 使用中发现的问题

当使用 MySQL 的 GROUP BY 语句时&#xff0c;根据指定的列对结果进行分组。在 GROUP BY 分组时&#xff0c;如果某个字段在分组中有多个不同的值&#xff0c;那么就会出现你提到的该字段一直在变化的情况。 这种情况通常是由于在 GROUP BY 中选择的字段与其他非聚合字段不兼容…

【链表OJ 11】复制带随机指针的链表

前言: &#x1f4a5;&#x1f388;个人主页:​​​​​​Dream_Chaser&#xff5e; &#x1f388;&#x1f4a5; ✨✨刷题专栏:http://t.csdn.cn/UlvTc ⛳⛳本篇内容:力扣上链表OJ题目 目录 leetcode138. 复制带随机指针的链表 1. 问题描述 2.代码思路: 2.1拷贝节点插入到…

操作系统强化认识之Shell编程学习与总结

目录 1.Shell的概述 2.Shell脚本入门 3.变量 3.1.系统预定义变量 3.2.自定义变量 3.3.特殊变量 4.运算符 5.条件判断 6.流程控制 6.1.if判断 6.2.case语句 6.3.for循环 6.4.while循环 7.read读取控制台输入 8.函数 8.1.系统函数 8.2.自定义函数 9.正则表示式入…

React原理 - React New Feature

目录 扩展学习资料 React Fragments/Portals/StrictMode【糖果】 Fragments【并列元素外包裹一个伪元素】 Portals【改变组件挂载节点】 Strict Mode【严格模式&#xff0c;老工程中使用&#xff0c;提示即将失效方法】 React Concurrent Mode【大招】 不可阻断渲染/可中…

【广州华锐互动】煤矿设备AR远程巡检系统实现对井下作业的远程监控和管理

煤矿井下作业环境复杂&#xff0c;安全隐患较多。传统的巡检方式存在诸多弊端&#xff0c;如巡检人员难以全面了解井下情况&#xff0c;巡检效率低下&#xff0c;安全隐患难以及时发现和整改等。为了解决这些问题&#xff0c;提高煤矿安全生产水平&#xff0c;越来越多的企业开…

C#winform自定义软键盘

软键盘应用 触摸一体机没有硬件键盘&#xff0c;系统软键盘占面积大&#xff0c;位于屏幕底部&#xff0c;点击不是很方便。某些时候只需要输入数字&#xff0c;这时弹出九宫格数字键盘就足够了。 以下实现的是弹出一个弹窗作为软键盘。 实现 创建一个窗体FrmSoftKey&#xf…

【Git-Exception】Git报错:fatal: unable to auto-detect email address

报错信息&#xff1a; *** Please tell me who you are. Run git config --global user.email “youexample.com” git config –global user.name “Your Name” to set your account’s default identity. Omit --global to set the identity only in this repository. fatal…

JVM:JIT实时编译器

一、相关 ⾼级编程语⾔按照程序的执⾏⽅式分为两种 编译型&#xff1a;一次性将代码编译为机器码解释型&#xff1a;通过解释器一句一句的将代码解释为机器码之后&#xff0c;再运行。每个语句都是执行的时候才翻译。 JAVA代码执行过程 &#xff08;编译阶段&#xff09;首先将…