Golang - slice 内部实现原理解析

news2024/11/20 8:28:50

Golang - slice 内部实现原理解析

一.Go中的数组和slice的关系

1.数组

在几乎所有的计算机语言中,数组的实现都是一段连续的内存空间,Go语言数组的实现也是如此,但是Go语言中的数组和C语言中数组还是有所不同的

  • C语言数组变量是指向数组第一个元素的指针
  • Go语言的数组是一个值,一个数组变量就代表整个数组,意味着Go语言的数组在传递的时候,传递是是原数组的拷贝!

这也就意味着数组在传递的时候,对大数组来说,内存代价会非常大,影响性能,传递数组指针可以解决这个问题,但是数组指针也有一个弊端:

  • 原数组的指针指向改变了,那函数里面的指针指向也会跟着改变,某些情况下,可能会产生意想不到的bug

slice的出现,便是为了解决这个问题

2.slice

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dQzZaZgn-1683360323739)(https://gitee.com/cqfbest/md/raw/master/img2//1301290-20220114172752683-1165682702.png)]

先来看一张图,上图中,ptr就是指向底层数组的指针,len是指slice的长度,cap是指slice的容量

  • slice本身并不是动态数组或者数组指针,它的内部实现是通过指针引用底层数组,设置相关的属性,将数据的读写操作限定在指定的区域内
  • slice本身是一个只读读写,你修改的是底层数组,而不是slice本身,其工作机制类似于数组指针的一种封装
  • slice是对数组中一个连续片段的引用,所以slice是一个引用类型

当然从宏观和使用上来说,你可以将slice当做一个长度可变的数组,类似C++的Vector。

二.slice的初始化方式

方式1:字面量

    s = s[2:4]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZRTLnKlD-1683360323740)(https://gitee.com/cqfbest/md/raw/master/img2//1301290-20220114172829533-471833808.png)]

指针指向s[2],容量是3,长度是2

需要注意的是,尽量不要采用字面量这种方式初始化slice,除非情况特殊,因为一个字面量数组可以初始化很多个slice,修改一个slice,会影响另一个slice的值,因为引用的都是同一个底层数组

比如下图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DpEW11hp-1683360323740)(https://gitee.com/cqfbest/md/raw/master/img2//1301290-20220114172858248-225632968.png)]

sliceA和sliceB都是同一个底层数组,并且有重叠的部分!Array[2],30

方式2:make

    s := make([]byte, 5)

img

最为安全的slice初始化方式,推荐使用,除非业务特殊你实在想让你的slice共用同一个底层数组,再补充一个图,来说明slice长度和容量的区别

img

长度4,代表此时4个元素,容量6,代表总共可以装6个元素,还有两个位置空闲

三.slice的扩容规则

slice可以理解为动态数组,既然是动态数组,那必然需要进行扩容,slice扩容遵循以下规则:

  • slice容量小于1024个元素,则扩容后容量直接翻倍,
  • slice容量不小于1024个元素,则每次增加原来容量是四分之一
  • 如果扩容后,还是比底层数组的容量小,那么slice的指针还是指向原来的底层数组。
  • 如果扩容后,超过了底层数组的容量,那么会开辟一块新内存,并将原来的值拷贝过来,这种情况,slice的任何操作都不会影响原底层数组

四. slice的拷贝

1.浅拷贝情况

  • 浅拷贝,拷贝的是地址,只是复制指向对象的指针
  • slice是引用类型数据,默认引用类型数据,全部都是浅拷贝,slice,Map等
    slice2 := slice1
  • slice1和slice2指向的都是同一个底层数组,任何一个数组元素被改变,都可能会影响两个slice
  • 在slice触发扩容操作前,slice1和slice2指向的都是相同数组,但在触发扩容操作后,二者指向的就不一定是相同的底层数组了,具体可参考上诉slice的扩容规则

2. 深拷贝情况

  • 深拷贝,拷贝的是数据本身,会创建一个新对象
    copy(slice2, slice1)  
  • 新对象和原对象不共享内存,在新建对象的内存中开辟一个新的内存地址,新对象的值修改不会影响原对象值,既然内存地址不同,释放内存地址时,可以分别释放

五. slice内存泄露情况

当slice的底层数组很大,但slice所取元素数量很小时,底层数组占据的大部分空间都是被浪费的

  • 比如b数组很大,slice a只引用了b很小的一部分,只要slice a还在,b数组就永远不会被回收,就是造成了内存泄露!
var a []int

func test(b []int) {
    a = b[:1] // 和b共用一个底层数组
    return
}

解决方法:

  • 不再引用b数组,将需要的数据复制到一个新的slice中,这样新slice的底层数组,就和b数组无任何关系了
var a []int

func test(b []int) {
    a = make([]int, 1)
    copy(a, b[:0])
    return
}

六. slice 非并发安全

slice不是并发安全的,要并发安全,有两种方法:

  • 加锁
  • channle

1.加锁

适合于对性能要求不高的场景,毕竟锁的粒度太大,这种方式属于通过共享内存来实现通信

func TestSliceConcurrencySafeByMutex(t *testing.T) {
    var lock sync.Mutex //互斥锁
    a := make([]int, 0)
    var wg sync.WaitGroup
    for i := 0; i < 10000; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            lock.Lock()
            defer lock.Unlock()
            a = append(a, i)
        }(i)
    }
    wg.Wait()
    t.Log(len(a)) 
    // equal 10000
}

2.channle

适合于对性能要求大的场景,channle就是专用于goroutine间通信的,这种方式属于通过通信来实现共享内存,而Go的箴言便是:尽量通过通信来实现内存共享,而不是通过共享内存来实现通信,推荐此方法!

func TestSliceConcurrencySafeByChanel(t *testing.T) {
    buffer := make(chan int)
    a := make([]int, 0)
    // 消费者
    go func() {
        for v := range buffer {
            a = append(a, v)
        }
    }()
    // 生产者
    var wg sync.WaitGroup
    for i := 0; i < 10000; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            buffer <- i
        }(i)
    }
    wg.Wait()
    t.Log(len(a)) 
    // equal 10000
}

七. 小结

根据上述内容,可以总结出以下几点:

  • 创建slice时应根据实际需要预分配容量,避免追加过程中频繁扩容,有助于性能提升
  • slice是非并发安全的,如要实现并发安全,请采用锁或channle
  • 大数组作为函数参数时,会复制整个数组,消耗过多内存,建议采用slice或指针
  • 如果只用到大的slice或数组的一部分,建议将需要部分复制到新的slice中取,减少内存占用
  • 多个slice指向相同的底层数组时,修改其中一个slice,可能会影响其他slice的值
  • slice作为参数传递时,比数组更为高效,因为slice本身的结构就比较小!所以你参数传递时,传slice和传slice的引用,其实开销区别不大
  • slice在扩容时,可能会发生底层数组的变更和内存拷贝

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

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

相关文章

鸿蒙Hi3861学习七-Huawei LiteOS(信号量)

一、简介 信号量&#xff08;Semaphore&#xff09;是一种实现任务间通信的机制&#xff0c;实现任务之间同步或临界资源的互斥访问。常用于协助一组相互竞争的任务来访问临界资源。 在多任务系统中&#xff0c;各任务之间需要同步或互斥实现临界资源的保护&#xff0c;信号量功…

阿里工作7年,肝到P8就靠这份学习笔记了,已助14个朋友拿到offer

​ 在阿里工作了7年&#xff0c;工作压力大&#xff0c;节奏快&#xff0c;但是从技术上确实得到了成长&#xff0c;尤其是当你维护与大促相关的系统的时候&#xff0c;熬到P8也费了不少心思。 技术的更新迭代越来越快&#xff0c;程序员或许是这个过程中最为挣扎的一波人。每…

第0章 学习之前的准备

突然想写点关于linux的东西&#xff0c;一是将自己几十年来零碎的知识作以串联&#xff0c;二是能为正在学习路上的新手作些指引。而恰好作者的孩子是一位初一的学生&#xff0c;我写的这些东西也正是我手把手教授他的&#xff0c;现在分享出来并且命名为《linux中学教程》&…

记一次SpringBoot应用性能调优过程

背景 使用SpringBoot、MyBatis-Plus开发一个接口转发的能&#xff0c;将第三方接口注册到平台中&#xff0c;由平台对外提供统一的地址&#xff0c;平台转发时记录接口的转发日志信息。开发完成后使用Jmeter进行性能测试&#xff0c;使用100个线程、持续压测180秒&#xff0c;…

Java中池化技术探讨

背景&#xff1a;在日常开发中&#xff0c;除了考虑IO操作、线程上下文切换、GC的影响性能外。还通过池化技术提高性能通过循环复用资源&#xff0c;降低资源创建和销毁带来的开销和损失&#xff0c;从而提高性能&#xff0c;例如对象池、内存池、线程池、连接池 一、对象池&a…

软件测试 - 测试用例设计方法之等价类划分和边界值分析

1. 等价类划分法 1.1 基本理论 等价类划分法是通过科学的方法找到具有共同特性的测试输入的集合&#xff0c;避免进行穷举测试&#xff0c;大大减少了测试用例的数量&#xff0c;从而提高测试效率。等价类划分法的典型应用场景就是输入框&#xff0c;适用于较少数量输入框的场…

晶振概述及工作原理

晶振在电路板中随处可见&#xff0c;只要用到处理器的地方就必定有晶振的存在&#xff0c;即使没有外部晶振&#xff0c;芯片内部也有晶振。 晶振概述 晶振一般指晶体振荡器。晶体振荡器是指从一块石英晶体上按一定方位角切下薄片&#xff08;简称为晶片&#xff09;&#xf…

虚拟服务器基础架构解决方案:用最小的工作量实现最大的价值

虚拟服务器基础架构解决方案&#xff1a;用最小的工作量实现最大的价值 一切皆可虚拟化&#xff01;包括服务器在内。NetApp 虚拟服务器基础架构解决方案有助于加快数据访问速度、构建创新服务并简化部署&#xff0c;从而实现最大价值。 为什么选择 NetApp 的虚拟服务器基础架…

pytorch矩阵乘法总结

1. element-wise&#xff08;*&#xff09; 按元素相乘&#xff0c;支持广播&#xff0c;等价于torch.mul() a torch.tensor([[1, 2], [3, 4]]) b torch.tensor([[2, 3], [4, 5]]) c a*b # 等价于torch.mul(a,b) # tensor([[ 2, 6], # [12, 20]]) a * torch.tenso…

详解C++类对象(上篇)——超详细

目录 一&#xff0c;面向对象&面向过程的认识(简单了解即可&#xff0c;逐步认识&#xff09; 二&#xff0c; 类 2.1 类的引入 2.2 类的定义 1. struct 2. class 类的两种定义方式&#xff1a; 2.3 封装&类的访问限定符 1. 封装概念 2. 类的访问限定符 …

低代码如何不写代码创建表单和维护表单

工作表新建与修改 新建工作表的流程包含 新建工作表/编辑公祖表为工作表添加字段&#xff0c;例如“员工档案”表中有姓名、性别、年龄等字段为字段设置属性工作表布局工作表预览、保存、关闭 1、新建工作表/修改工作表 新建工作表 修改工作表 2、为工作表添加字段 添加字段 左…

关于C语言的一些杂记2

文章目录 sizeof运算符内容关于基本概念的问题关于一些语句的理解和分号的注意字符的理解关于输出格式的扩展 本文内容摘自C技能树一些优秀的博主 sizeof运算符内容 关于基本概念的问题 sizeof是C语言的关键字&#xff0c;它用来计算变量&#xff08;或数据类型&#xff09;在…

2.Hive创建数据库

1.数据库操作 1.1 创建数据库 create database test comment Just for test location /abcd with dbproperties(aaabbb); comment后面指的是注释&#xff1b;location后面是数据库存放路径&#xff1b;dbproperties代表了数据库的属性 ps.避免要创建的数据库已经存在错误&…

Vue最新状态管理工具Pinia——Pinia的安装与使用

Pinia从了解到实际运用——pinia的安装与使用 知识回调&#xff08;不懂就看这儿&#xff01;&#xff09;场景复现一、环境搭建1.创建项目2.安装pinia 二、基本使用1.创建pinia示例并挂载2.基本使用访问state使用getters使用actions 3.详细示例&#xff08;详细注解&#xff0…

【23】核心易中期刊推荐——视觉/图像感知与识别人工智能算法及应用​​​​​​​

🚀🚀🚀NEW!!!核心易中期刊推荐栏目来啦 ~ 📚🍀 核心期刊在国内的应用范围非常广,核心期刊发表论文是国内很多作者晋升的硬性要求,并且在国内属于顶尖论文发表,具有很高的学术价值。在中文核心目录体系中,权威代表有CSSCI、CSCD和北大核心。其中,中文期刊的数…

2023年盐城工学院五年一贯制专转本旅游学概论考试大纲

2023年盐城工学院五年一贯制专转本旅游学概论考试大纲 一、考核对象 本课程的考核对象是五年一贯制高职专升本酒店管理专业考生。 二、考核方式 本课程考核采用闭卷考试的方式。 三、考核要求 掌握旅游学的基本原理&#xff0c;掌握旅游学的核心概念&#xff0c;具备旅游…

Android性能监控:主循环性能统计LooperStatsService详解

作者&#xff1a;飞起来_飞过来 简介 在Android性能监控和优化领域&#xff0c;一个会影响App性能表现的因素与Handler Message Looper机制有关。当Looper里面的Message处理不及时、或数量太多占用过多处理时间时&#xff0c;可能会出现卡顿感&#xff0c;并且不容易定位到卡顿…

WoShop多商户进口出口跨境电商uniapp商城源码

源码介绍&#xff1a;WoShop多商户跨境电商商城系统将传统的分销、积分、拼团等传统销售模式和直播带货、短视频带货等新型电商营销完美融为一体&#xff0c;专注技术&#xff0c;支持二次开发&#xff0c;专为用户、技术商提供跨境电商技术解决方案。 WoShop跨境电商源码产品…

网络弹性基础知识和实践

什么是网络弹性 弹性是网络处理中断并继续以可接受的标准向用户提供服务的能力。网络运营可能会受到配置错误、断电或操作员错误等问题的威胁。当这种可能性发生时&#xff0c;最终用户无法访问网络&#xff0c;从而对组织产生负面影响。高度弹性的网络可以通过在网络运行中断…

chatgpt官网拒绝访问怎么处理-chatGPT入口正确打开方式

chatgpt官网拒绝访问的原因有哪些 OpenAI是一家人工智能技术公司&#xff0c;其官网是OpenAI最重要的宣传与交流平台之一。但是&#xff0c;有时访问OpenAI官网可能会受到限制或拒绝访问。以下是可能导致OpenAI官网拒绝访问的几个常见原因&#xff1a; IP地址被封锁: OpenAI网…