Golang — map的使用心得和底层原理
map作为一种基础的数据结构,在算法和项目中有着非常广泛的应用,以下是自己总结的map使用心得、实现原理、扩容机制和增删改查过程。
1.使用心得:
1.1 当map为nil和map为空时,增删改查操作时会出现的不同情况
我们可以发现,当一个map为空或者为nil的时候,直接对其值进行打印输出并没有什么不同,都为map[ ]。但是当我们打印内存地址的时发现,map为空时,是有指针指向的一块内存空间的;map为nil时,是一个空指针,表示此时并没有进行内存空间的开辟。这也就导致了我们对值为nil的map做增、改操作时会触发panic,导致程序直接退出。
1.2 map初始化
map初始化有两种方法,一种是字面量初始化,一种是内置函数make()初始化。在使用内置函数make()初始化的时候,我们可以预先指定容量大小,减少后期map扩容带来的内存消耗。
1.3 map是无序的
map中存储的键值对,在取出的时候时没有顺序的,每次遍历取出的顺序都是不一致的,因此不要使用map存储一些顺序性的操作。如果需要进行顺序存储,请使用切片。
func main() {map01 := make(map[int]int)map01[1] = 1map01[2] = 2map01[3] = 3map01[4] = 4for key, value := range map01 {fmt.Println(key, value)}/*输出结果:4 41 12 23 3*/
}
1.4 并发读写不安全
由于map的增删改查的操作并不是原子性的,因此当多个协程并发访问map的时候,会导致读写冲突,引发panic导致程序中断。Go语言团队在设计map的时候,认为map在大多数场景下是没有并发读写需求的,如果为了实现并发读写,而在map中引入锁,会降低操作性能,得不偿失。虽然map没有实现并发读写机制,但是go语言团队在map中引入了并发检测机制,一旦发现多个协程并发读写map的时候,会立即panic,以免隐藏错误。如果实现需要在并发场景下使用map,可以使用sync.map,进行并发控制。
2.实现原理:
得Go语言中的map是基于hash表实现的,hash表是一种常见的数据结构,用来存储键值对类型的数据。我们通常将key经过哈希函数的运算之后到hash值,然后将value存储在hash值对应的内存地址上。通过hash函数我们实现了从key到hash值的映射,可以通过key来快速获取对应的value。
map实现核心其实就是以下几点:
- hash函数
- hash冲突的解决
- key对应着的value的查找过程
关于hash表,不是很懂的小伙伴可以查看这篇文章:
关于Hash表,你不得不知道的知识点http://t.csdnimg.cn/XigRT
2.1 hmap结构体
// Go map的头文件。
type hmap struct {count int // 当前保存的元素个数B uint8 // bucket数组的大小noverflow uint16 // 溢出桶的大概数目hash0 uint32 // 哈希种子buckets unsafe.Pointer // bucket数组,数组长度为2^B,如果count=0的时候,桶可能为nil。oldbuckets unsafe.Pointer // buckets桶的数量的一半,用于做map扩容是,存放旧的数据,一旦数据迁移完毕后,置为nil....................
}
2.2 bmap结构体
// Go语言中map的桶
type bmap struct {tophash [bucketCnt]uint8 //tophash通常包含哈希值的第一个字节(高8位)//注意:把所有的键放在一起,然后把所有的元素放在一起//采用key/elem/key/elem/…的形式,减少字节对齐带来的空间损耗。例如map[int64]int8,//一个溢出指针,bmap类型的溢出指针
}
在bmap中有两个隐藏的字段,没有显式地在结构体中声明,根据运行时指针的偏移来访问这些虚拟成员。其中,两个虚拟成员的作用是:
一个是用来存放真实的key和value的,采用key/key/key……value/value/value……的形式进行存储,最多可以存储8个键值对。
另一个用来存储哈希冲突的溢出字段,用指针将所有的溢出字段连接在一起。
go语言中的map采用下图的结构组织起来。一个Hash表里面有多个bucket,每一个bucket保存了map中的一个或者一组键值对。其中,一组键值对最多有八个。
当有两个或以上数量的键被哈希到了同一个bucket时,我们称这些键发生了冲突。Go使用链地址法来解决键冲突。 由于每个bucket可以存放8个键值对,所以同一个bucket存放超过8个键值对时就会再创建一个键值对,用类似链表的方式将bucket连接起来。
3.扩容机制:
由于Hash冲突的存在,多个不同的key值,可能被放入少数bucket中,从而使hash值不均匀地分布桶中,导致bucket中使用了大量overflow指针来链接冲突的键值对,降低读取效率。
我们通常使用负载因子来衡量一个Hash表的冲突情况,其公式为:
负载因子 = 键数量 / bucket数量
例如,对于一个键数量为8,bucket数量为4的Hashb表来说,其负载因子为8/4=2.
负载因子过大过小都不理想:
- 负载因子过小,说明空间利用率低;
- 负载因子过大,说明哈希冲突严重,存取效率低,需要在多个overflow中进行链表查询操作。
负载因子过小,可能使预分配的空间太大,也可能是大部分的元素被删除造成的。随着元素不断添加到map中,负载因子会逐渐地升高。
当Hash表的负载因子过大时,需要申请更多的bucket,来降低负载因子;当负载因子过小时,Hash表中可能存在大量的overflow溢出桶,读取效率差。为了保证存取效率,会对所有的键值对进行重新组织,使其均匀地分布在这些bucket中,这个过程成为rehash。
3.1 扩容的条件:
Go语言会根据负载因子的大小,进行扩容操作,扩容有两种类型,一种是增量扩容,一种是等量扩容。增量扩容发生于bucket桶少,键值对多的情况,这时候增加桶的数量,即可降低负载因子。等量扩容发生在一个表进行了大量的删除操作,此时键值对零散地分布在各个溢出的桶中,我们为了提高存取效率,需要对hash表重新进行组织,删除一些overflow溢出桶。以下是Hash表的扩容条件:
- 当一个负载因子过大时,负载因子大于6.5,则需要进行增量扩容。
- 当一个负载因子过小时,overflow的数量超过2^min(B,15)时,则会进行等量扩容。
3.2 增量扩容:
当负载因子过大时,就新建一个bucket,新的bucket长度是原来的2倍,然后旧bucket数据搬迁到新的bucket。 考虑到如果map存储了数以亿计的key-value,一次性搬迁将会造成比较大的延时,Go采用逐步搬迁策略,即每次访问map时都会触发一次搬迁,每次搬迁2个键值对。
下图展示了包含一个bucket满载的map(为了描述方便,图中bucket省略了value区域):
当前map存储了6个键值对,只有1个bucket。此时负载因子为6。再次插入数据时将会触发扩容操作,扩容之后再将新插入键写入新的bucket。
当第7个键值对插入时,将会触发扩容,扩容后示意图如下:
hmap数据结构中oldbuckets成员指身原bucket,而buckets指向了新申请的bucket。新的键值对被插入新的bucket中。 后续对map的访问操作会触发迁移,将oldbuckets中的键值对逐步的搬迁过来。当oldbuckets中的键值对全部搬迁完毕后,删除oldbuckets。
搬迁完成后的示意图如下:
3.3 等量扩容:
所谓等量扩容,实际上并不是扩大容量,buckets数量不变,重新做一遍类似增量扩容的搬迁动作,把松散的键值对重新排列一次,以使bucket的使用率更高,进而保证更快的存取。 在极端场景下,比如不断的增删,而键值对正好集中在一小部分的bucket,这样会造成overflow的bucket数量增多,但负载因子又不高,从而无法执行增量搬迁的情况,如下图所示:
上图可见,overflow的buckt中大部分是空的,访问效率会很差。此时进行一次等量扩容,即buckets数量不变,经过重新组织后overflow的bucket数量会减少,即节省了空间又会提高访问效率。
4.增删改查过程:
4.1 查
- 根据key值,计算对应的hash值
- 取hash值低八位与hmap.B取模来确定桶的位置,这就是桶定位操作。
- 取hash值的高八位,在tophash数组中查询,如果tophash[i]存储的hash值与当前key对应的hash值相等,则获取tophash[i]的key值进行比较。【不仅仅要hash值相同,对应的key值也要相同】
- 如果在当前bucket中没有找到,则依次从溢出的bucket中查找。
- 如果当前bucket正在搬迁的过程中,则优先从oldbuckets中进行查找,如果找不到,再去buckets中进行查找。
- 如果最后查询不到,则返回相应类型的零值。
4.2 增
- 根据key值算出hash值
- 取Hash值的低八位与hmap.B取模来进行桶定位,确定要插入元素的桶
- 查找该key是否已经存在,如果存在则直接更新值
- 如果不存在,则从给bucket中寻找空余位置并插入
如果当前map处于搬迁过程中,则新元素会直接添加到新的buckets数组中,但查找过程仍然从oldbuckets开始查找。
4.3 改
更改插操作实际上就是一种特殊的增加操作,如果元素不存在,更改操作等同于添加操作。
4.4 删
删除操作其实等同于查询操作,如果查找到该元素,则直接进行删除,如果查找不到,则执行一次空操作。
相关文章:

Golang — map的使用心得和底层原理
map作为一种基础的数据结构,在算法和项目中有着非常广泛的应用,以下是自己总结的map使用心得、实现原理、扩容机制和增删改查过程。 1.使用心得: 1.1 当map为nil和map为空时,增删改查操作时会出现的不同情况 我们可以发现&#…...

Oracle如何收缩减小表空间大小
比如我们发现一个表空间占用比较大,但是空闲空间很大,想要减小表空间占用大小。查看表空间的情况 发现BETEST表空间占用大,但是剩余大小比较大,可以减小存储占用。 如果我们想减小到100MB,那么就登录其用户执行&#…...

【爬虫】爬取股票历史K线数据写入数据库(三)
前几天有写过两篇: 【爬虫】爬取A股数据写入数据库(二) 【爬虫】爬取A股数据写入数据库(一) 现在继续完善,分析及爬取股票的历史K线数据通过ORM形式批量写入数据库。 2024/05,本文主要内容如下…...

文心一言指令
文心一言(ERNIE Bot)是百度公司开发的人工智能语言模型,它可以接收各种指令来执行不同的任务。以下是一些可能的指令示例: 知识问答: 指令:“请问什么是人工智能?”文心一言会回答关于人工智能…...

常用的命令技巧总结
java命令执行 如下编码网站: Runtime.exec Payload Generater | AresXs Blogjava.lang.Runtime.exec() Payload Workarounds - Jackson_Thttps://www.bugku.net/runtime-exec-payloads/ 手动编码操作 bash -c {echo,cGluZyAxMjcuMC4wLjE7ZWNobyAxID50ZXN0LnR4dA}|…...

T97燃脂咖啡招商模式,私域分销模式设计
t97燃脂咖啡招商模式,希柔T97微商模式,社交电商系统 坐标:厦门,我是易创客肖琳 深耕社交新零售行业10年,主要提供新零售系统工具及顶层商业模式设计、全案策划运营陪跑等。 低卡咖啡第一品牌“T97”,突然…...

触摸OpenNJet,感悟云原生
小程一言 云原生使得应用充分利用云计算、容器化和微服务架构等现代技术来构建和运行应用程序。 云原生技术的用处在于提高应用程序的可靠性、可伸缩性和灵活性,加快开发和部署速度,降低成本,提升整体的效率和竞争力。通过采用云原生技术&a…...

UE4 自定义shader获取灯光位置
UE4.26:How to get the direction of specific directional lights in custom node material? - #4 by Arkiras - Rendering - Epic Developer Community Forums 获取灯光位置的shader,应该是这个了...
机器学习(五) ----------决策树算法
目录 1 核心思想 2 决策树算法主要步骤 3 决策树算法的分类 3.1 ID3算法(Iterative Dichotomiser 3): 3.1.1 基本步骤 3.1.2 原理 信息增益 3.1.3 注意事项 3.2 C4.5算法: 3.2.1. 信息增益率 计算公式 3.2.2. 构建决策…...

Redis的数据完全是存在内存中的吗?
是的,Redis的数据完全是存储在内存中的。这也是Redis能够提供非常高速的读写性能的主要原因,尤其适用于需要快速响应的应用场景。然而,虽然Redis将所有数据存储在内存中,但它也提供了持久化机制,可以将数据异步地保存到…...

Linux开发--Linux设备驱动核心
Author: cpu_codeDate: 2020-06-30 16:15:35LastEditTime: 2020-07-01 17:59:23FilePath: \md\Linux\6818_Linux驱动.mdGitee: https://gitee.com/cpu_codeCSDN: https://blog.csdn.net/qq_44226094 Linux设备驱动核心概念 Linux内核中断处理 Linux操作系统下同裸机程序一样…...

vue3引入vant完整步骤
在Vue 3中引入Vant(一个基于Vue的移动端UI组件库)的完整步骤通常包括以下几个部分: 安装Vue CLI(如果你还没有安装的话): npm install -g vue/cli 创建一个新的Vue项目: 假设你希望项目名为my…...

C语言——文件缓冲区
一、用户缓冲区和系统缓冲区 缓冲区的概念确实可以分为多个层次,其中最常见的两个层次是用户缓冲区和系统缓冲区。 这里的用户缓冲区和系统缓冲区都包括输入输出缓冲区。 1、用户缓冲区(User-space Buffer) 用户缓冲区是指由用户程序&…...

如何快速检测原理图中的元器件与PLM系统的一致性,提高原理图设计准确性
背景介绍 保证原理图中的元器件来源于公司的PLM系统、ERP系统的,是输出有效BOM的根源,初始BOM的准确率,能大大降低ECN的数量,提高生产备料的时效,缩短采购周期。 然而,原理图设计过程中,由于…...

英特尔处理器排行
英特尔的处理器性能排行通常是根据其发布的不同代数和型号来划分的,以下是一些高性能的英特尔处理器: Intel 酷睿 i9 14900K:这是目前英特尔桌面平台中的旗舰处理器之一,提供了极高的性能,适合高端游戏和专业工作负载…...

【日志革新】在ThinkPHP5中实现高效TraceId集成,打造可靠的日志追踪系统
问题背景 最近接手了一个骨灰级的项目,然而在项目中遇到了一个普遍的挑战:由于公司采用 ELK(Elasticsearch、Logstash、Kibana)作为日志收集和分析工具,追踪生产问题成为了一大难题。尽管 ELK 提供了强大的日志分析功…...

英译汉早操练-(二十)
hello大家好,这篇跟随十九,继续真题学习。如果想看全部请返回到第十九篇。 英译汉早操练-(十九)-CSDN博客 The political upheaval in Libya and elsewhere in North Africa has opened the way for thousands of new migrants to…...

Go-Zero自定义goctl实战:定制化模板,加速你的微服务开发效率(四)
前言 上一篇文章带你实现了Go-Zero和goctl:解锁微服务开发的神器,快速上手指南,本文将继续深入探讨Go-Zero的强大之处,并介绍如何使用goctl工具实现模板定制化,并根据实际项目业务需求进行模板定制化实现。 通过本文…...

(五)STM32F407 cubemx IIC驱动OLED(1)IIC协议篇
(五)STM32F407 cubemx IIC驱动OLED(1)IIC协议篇 这篇文章主要是个人的学习经验,想分享出来供大家提供思路,如果其中有不足之处请批评指正哈。 废话不多说直接开始主题,本人是基于STM32F407V…...

OpenCV特征匹配总结
1.概述 在深度学习出现之前,图像中的特征匹配方法主要有 2.理论对比 3.代码实现 #include <iostream> #include <opencv2/opencv.hpp>int main(int argc, char** argv) {if(argc ! 3) {std::cerr << "Usage: " << argv[0] <…...

二叉树的四种遍历代码实现
二叉树的遍历大致能分为以下几种 1.前序:根 左 右 2.中序:左 根 右 3.后序:左 右 根 4.层序:从根开始一层一层的向下 如上图访问顺序: 前序:1 2 3 N N N 4 5 N N 6 N N 中序:N 3 N 2 N 1 N 5 N 4 N …...

系统和功能测试:确保软件的功能和易用性
目录 概述 功能测试 LOSED 模型 用例的设计 等价类划分 边界值分析 循环结构测试的综合方法 因果图 决策表 功能图 正交实验设计 易用性测试 内部易用性测试 外部易用性测试 功能性测试 正向功能性测试 负向功能性测试 功能性测试工具 结语 概述 在软件开发…...

关于服务端接口知识的汇总
大家好,今天给大家分享一下之前整理的关于接口知识的汇总,对于测试人员来说,深入了解接口知识能带来诸多显著的好处。 一、为什么要了解接口知识? 接口是系统不同模块之间交互的关键通道。只有充分掌握接口知识,才能…...

树(数据结构)
树的定义 一个根结点,其余结点分为 m 个不相交的集合, 其中每个集合本身又是一棵树,并且称为根的子树。 树的根结点没有前驱,其他结点有且仅有一个前驱。 所有结点可以有0个或多个后继。 基本术语 结点的度 树的度 : 树…...

Spring底层入门(十一)
1、条件装配 在上一篇中,我们介绍了Spring,Spring MVC常见类的自动装配,在源码中可见许多以Conditional...开头的注解: Conditional 注解是Spring 框架提供的一种条件化装配的机制,它可以根据特定的条件来控制 Bean 的…...

优质资料:大型制造企业等级保护安全建设整改依据,系统现状分析,网络安全风险分析
第1章 项目概述 XX 大型制造型企业是国内一家大型从事制造型出口贸易的大型综合企业集团,为了落实国家及集团的信息安全等级保护制度,提高信息系统的安全防护水平,细化各项信息网络安全工作措施,提升网络与信息系统工作的效率&am…...

几种监控工具学习
在Linux上有很多监控工具,比如Zabbix、Prometheus、APM和ELK 监控工具是确保系统稳定运行的关键组件之一,它可以帮助系统管理员和开发人员及时发现并解决问题。 以下是几种流行的监控工具的简要介绍: Zabbix: Zabbix 是一个企…...

树莓派python开发
树莓派自带thonny 点亮LED灯 import RPi.GPIO as GPIO import time# 设置GPIO模式为BCM GPIO.setmode(GPIO.BCM)# 设置LED引脚 led_pin 18# 设置LED引脚为输出 GPIO.setup(led_pin, GPIO.OUT)# 点亮LED GPIO.output(led_pin, GPIO.HIGH)# 延时2秒 time.sleep(2)# 关闭LED GPI…...

纯血鸿蒙APP实战开发——首页下拉进入二楼效果案例
介绍 本示例主要介绍了利用position和onTouch来实现首页下拉进入二楼、二楼上划进入首页的效果场景,利用translate和opacity实现动效的移动和缩放,并将界面沉浸式(全屏)显示。 效果图预览 使用说明 向下滑动首页页面超过触发距…...

苹果cms:开启高速缓存加快访问速度
由于苹果cms采集的影片数据过多,如果不设置缓存,可能会造成网站访问缓慢,或者CPU消耗过高。随着用户访问量的上升,添加缓存设置是有这个必要的。目前cms提供了四种缓存方式 1)file:以文件形式,通俗说直接访问Mysql,要达…...