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] = 1
	map01[2] = 2
	map01[3] = 3
	map01[4] = 4
	for key, value := range map01 {
		fmt.Println(key, value)
	}
	/*
    输出结果:
	4 4
	1 1
	2 2
	3 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实现核心其实就是以下几点:

  1. hash函数
  2. hash冲突的解决
  3. key对应着的value的查找过程

关于hash表,不是很懂的小伙伴可以查看这篇文章:

关于Hash表,你不得不知道的知识点icon-default.png?t=N7T8http://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 查

  1. 根据key值,计算对应的hash值
  2. 取hash值低八位与hmap.B取模来确定桶的位置,这就是桶定位操作。
  3. 取hash值的高八位,在tophash数组中查询,如果tophash[i]存储的hash值与当前key对应的hash值相等,则获取tophash[i]的key值进行比较。【不仅仅要hash值相同,对应的key值也要相同】
  4. 如果在当前bucket中没有找到,则依次从溢出的bucket中查找。
  5. 如果当前bucket正在搬迁的过程中,则优先从oldbuckets中进行查找,如果找不到,再去buckets中进行查找。
  6. 如果最后查询不到,则返回相应类型的零值。

4.2 增

  1. 根据key值算出hash值
  2. 取Hash值的低八位与hmap.B取模来进行桶定位,确定要插入元素的桶
  3. 查找该key是否已经存在,如果存在则直接更新值
  4. 如果不存在,则从给bucket中寻找空余位置并插入

如果当前map处于搬迁过程中,则新元素会直接添加到新的buckets数组中,但查找过程仍然从oldbuckets开始查找。

4.3 改

更改插操作实际上就是一种特殊的增加操作,如果元素不存在,更改操作等同于添加操作。

4.4 删

删除操作其实等同于查询操作,如果查找到该元素,则直接进行删除,如果查找不到,则执行一次空操作。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于C++和Python基础的Golang学习笔记

文章目录 一、基础1.DOS命令2.变量(1)局部变量(2)全局变量(3)数据类型(4)指针(5)运算符(6)自定义数据类型 3.语句(1&#…

Navicat 干货 | 探索 PostgreSQL 中不同类型的约束

PostgreSQL 的一个重要特性之一是能够对数据实施各种约束,以确保数据完整性和可靠性。今天的文章中,我们将概述 PostgreSQL 的各种约束类型并结合免费的 "dvdrental" 示例数据库 中的例子探索他们的使用方法。 1. 检查约束: 检查…

Virtualbox7.0.10+Ubuntu20.04网络配置

虚拟机部署在服务器上时,需要进行网络配置,使虚拟机和服务器在同网段下,以保证内网的终端可以访问到虚拟机 1. 设置虚拟机 打开虚拟机设置,选择“网络”,将网卡设为桥接网卡 注:设置前,需要先…

[通用人工智能] 论文分享:ElasticViT:基于冲突感知超网的快速视觉Transformer

引言: 近年来,视觉Transformer(Vision Transformer,简称ViT)在计算机视觉任务中的应用日益广泛,从图像分类到对象识别等,均显示出优越的性能。然而,ViT模型也面临一些挑战,特别是在模…

抽丝剥茧:详述一次DevServer Proxy配置无效问题的细致排查过程

事情的起因是这样的,在一个已上线的项目中,其中一个包含登录和获取菜单的接口因响应时间较长,后端让我尝试未经服务转发的另一域名下的新接口,旧接口允许跨域请求,但新接口不允许本地访问(只允许发布测试/生…

ARM架构安全特性概览

安全之安全(security)博客目录导读 目录 一、跨行业计算安全 二、Arm架构安全特性的益处 三、安全威胁与缓解 四、防御执行技术 五、隔离技术 六、通用平台安全服务 七、标准安全 API 八、PSA安全标准认证 一、跨行业计算安全 从一开始,Arm 生态系统一直是…

VS项目Debug下生成的EXE在生产机器上运行

使用Visual Studio开发应用程序时,为了临时在非开发机上看一下效果,就直接把Debug下的文件全部拷贝到该机器上,直接双击exe运行。双击之后,没有直接打开应用程序,而是弹出了一个Error弹框。  赶快在网上搜了一遍&…

Ardupilot开源代码之Rover上路 - 后续1

Ardupilot开源代码之Rover上路 - 后续1 1. 源由2. 问题汇总2.1 问题1:飞控选择2.2 问题2:飞控安装位置和固定2.3 问题3:各种插头、插座配套2.4 问题4:分电板缺陷2.5 问题5:电机编码器接线及正反向问题2.6 问题6&#x…

什么是等保2.0,相对等保1.0有哪些变化,支撑等保2.0的标准文档有哪些?

1. 等保1.0、等保2.0业界定义 等保1.0:以1994年2月18日年国务院颁布的 147 号令《中华人民共和国计算机信息系统安全保护条例》为指导标准,以2008年发布的《GB/T22239-2008 信息安全技术 信息系统安全等级保护基本要求 》为指导的网络安全等级保护办法。…

向量数据库:Chroma

目录 一、Chroma 介绍 二、安装 Chroma 2.1 创建虚拟 python 环境 2.2 安装 Chroma 2.3 运行 Chroma 三、Backend API 一、Chroma 介绍 Chroma是一个开源的嵌入式数据库。Chroma通过使知识(knowledge)、事实(facts)和技能(skills)可插拔,从而简化了大型语言模…

小猫咪邮件在线发送系统源码,支持添加附件

一款免登录发送邮件,支持发送附件,后台可添加邮箱,前台可选择发送邮箱 网站数据采取本地保存,所以使用前请给网站修改权限,否则很多功能将无法使用 安装教程: 1.上传服务器或者主机 2.登录后台,添加发送…

FCOS长文详解

1. 概述 FCOS是一种one-stage、全卷积(Fully Convolutional)结构的目标检测模型,发表于2019年ICCV。(什么是one-stage?) 论文原地址:https://arxiv.org/abs/1904.01355 作者源码:ht…

本地项目上传到gitee

1. 新建仓库,不要勾选 2. git init git add . git commit -m "test" git remote add origin 【url】 git push --set-upstream origin master

【Java】:方法重写、动态绑定和多态

目录 一个生动形象的例子 场景设定 1. 方法重写(Method Overriding) 2. 动态绑定(Dynamic Binding) 3. 多态(Polymorphism) 归纳关系: 重写 概念 条件 重写的示例 重载与重写的区别 …

STM32 | STC-USB驱动安装Windows 10(64 位)

Windows 10(64 位)安装方法 由于 Windows10 64 位操作系统在默认状态下,对于没有数字签名的驱动程序是不能安装成功的。所以在安装 STC-USB 驱动前,需要按照如下步骤,暂时跳过数字签名,即可顺利安装成功。…

回归预测 | Matlab实现GA-LSSVM遗传算法优化最小二乘支持向量机多输入单输出回归预测

回归预测 | Matlab实现GA-LSSVM遗传算法优化最小二乘支持向量机多输入单输出回归预测 目录 回归预测 | Matlab实现GA-LSSVM遗传算法优化最小二乘支持向量机多输入单输出回归预测预测效果基本介绍模型描述程序设计参考资料 预测效果 基本介绍 Matlab实现GA-LSSVM遗传算法优化最小…

纯CSS实现步骤条

纯CSS实现纵向Steps步骤条效果 效果图 实现思路 步骤条是一种用于引导用户按照特定流程完成任务的导航条,在各种分步表单交互场景中广泛应用。步骤条通常由编号、名称和引导线三个基本要素组成。本文中要实现的是一个简单的步骤条,包含上述三个基本要素…

Python爬虫实战:爬取【某旅游交通出行类网站中国内热门景点】的评论数据,使用Re、BeautifulSoup与Xpath三种方式解析数据,代码完整

一、分析爬取网页: 1、网址 https://travel.qunar.com/2、 打开网站,找到要爬取的网页 https://travel.qunar.com/p-cs299979-chongqing进来之后,找到评论界面,如下所示:在这里我选择驴友点评数据爬取点击【驴友点评…

天下大爱唯母爱

岁月轮转,人生寻常,又逢一年母亲节。作为子女,这是所有人都参与节日,也是每一位母亲在繁忙日常中,一个短暂的休息,停下手中的忙碌,听孩子的一声祝福:妈妈辛苦了,母亲节快…

树莓派4B-搭建一个本地车牌识别服务器

实现目标: 一、设备自启后能够获得服务的ip与端口号,用于计算机连接设备; 二、计算机可以通过服务ip与端口访问设备服务; 三、上传需要处理的数据,返回结果反馈给用户; 四、上传到服务器的数据不会导致设备…