Go常见数据结构的实现原理——map

(一)基础操作

版本:Go SDK 1.20.6

1、初始化

map分别支持字面量初始化和内置函数make()初始化。

字面量初始化:

	m := map[string] int {"apple": 2,"banana": 3,}

使用内置函数make()初始化:

	m := make(map[string]int,10)	// 指定容量可以有效减少内存分配次数,有利于提升程序性能m["apple"] = 2m["banana"] = 3

注意:未初始化的map变量的默认值为nil,向值为nil的map添加元素时会触发panic:assignment to entry in nil map(赋值给空的map),如:

	var m map[string]intm["apple"] = 2					// 触发panic	

在这里插入图片描述

2、增删改查

map的增删改查比较随意…

	m := make(map[string]int,10)m["apple"] = 2			// 添加m["apple"] = 3			// 修改delete(m,"apple")		// 删除v := m["apple"]			// 查询v,exist := m["apple"]	// 查询if exist {fmt.Println(v)}

这里有几个需要注意的地方:

  1. 在上面的修改操作中,如果键"apple"不存在,则会直接执行添加操作。
  2. 删除元素使用内置函数delete()完成,delete()没有返回值,在map为nil或指定的键不存在的情况下,delete()也不会报错,相当于空操作。
  3. 如果使用的是第一种方式查询,当key不存在时,会返回value对应的零值,比如上面会返回0。当使用第二种时,第一个变量为值,第二个为bool类型的变量,用于指示是否存在指定的键,如果键不存在,那么第一个值为同样为对应零值。
  4. map操作不是原子的,当多个协程同时操作map时有可能会产生读写冲突,读写会触发panic

内置函数len()可以查询map的长度,该长度反应map中存储的键值对数。

(二)实现原理

1、数据结构

Go语言的map使用Hash表作为底层实现,一个Hash表里可以有多个bucket,而每个bucket保存了map中的一个或一组键值对。

(1)map的数据结构

map的数据结构由 runtime/map.go:hmap 定义:

type hmap struct {count     int	 	// 当前保存的元素个数flags     uint8		// 状态标志B         uint8  	// bucket 数组的大小noverflow uint16 	// 溢出桶的大概数量hash0     uint32 	// 哈希种子buckets    unsafe.Pointer // bucket 数组,数组的长度为2^Boldbuckets unsafe.Pointer // 老旧bucket数组,用于扩容nevacuate  uintptr        // 表示扩容进度,小于此地址的buckets代表已搬迁完成extra *mapextra // optional fields
}

下图展示了一个hmap.B=2t的map。

在这里插入图片描述

(2)bucket的数据结构

bucket(桶)数据结构由runtime/map.go:bmap定义

type bmap struct {tophash [bucketCnt]uint8	// 长度为8的数组
}
// 底层定义的常量
const (bucketCntBits = 3bucketCnt     = 1 << bucketCntBits		// 一个桶最多有8个位置
)

这是我在书上看到的bucket数据结构,并做出了如下解释:
bucket数据结构中的data和overflow成员并没有显示地在结构体中声明,运行时在访问bucket时直接通过指针的偏移量来访问这些虚拟成员

type bmap struct {tophash [8]uint8	// 存储Hash值的高8位data	[]byte		// key value 数据:key/key/key/.../value/value/value...overflow *bmap		// 溢出bucket的地址
}

每个bucket可以存储8个键值对

  • tophash 是一个长度为8的整型数组,Hash值低位相同的键存入当前bucket时会将Hash值的高位存储在数组中,以方便后续匹配。
  • data 区存放的是key-value数据,存放顺序是 key/key/key/…/value/value/value,如此存放是为了节省字节对齐带来的空间浪费。
  • overflow 指针指向的是下一个bucket,据此将所有冲突的键连接起来。
    在这里插入图片描述

所以tophash到底有什么用?

具体来说,如果两个键的哈希值的低位相同,但高位不同,它们可能会被映射到同一个桶位置。为了区分它们,可以将高位存储在 tophash[i] 数组中。这样,在查找时,可以首先比较低位哈希值,如果相等,再比较高位,以确保正确地匹配到相应的键。

在这种情况下,当添加元素时,如果 tophash[i] 中存储的哈希值与当前 key 的哈希值不相等,可能表示哈希冲突。这时,可能需要通过线性搜索或其他冲突解决方法在当前桶中查找匹配的键。在查找的过程中,可以利用 tophash[i] 数组中的高位信息来进一步确保正确匹配。

总体而言,这种做法是一种提高哈希表性能的优化策略,通过更多的信息来区分相同低位哈希值的键,以减少哈希冲突的影响。在实现哈希表时,具体的优化方法可能会因语言或库的不同而有所不同。

2、哈希冲突

当有两个或以上数量的键被“Hash”到同一个bucket时,我们称这些键发生了冲突。Go使用链地址法来解决冲突。
关于哈希冲突的详细解释可以移步我的这篇博客哈希表是什么
在这里插入图片描述

3、负载因子

负载因子用于衡量一个Hash表冲突情况,公式为:

负载因子 = 键数量/bucket数量

负载因子过小或过大都不理想:

  • 负载因子过小,说明空间利用率低。
  • 负载因子,说明冲突严重,存取效率低

当Hash表的负载因子过大时,需要申请更多的bucket,并对所有的键值对重新组织,使其均匀地分布到这些bucket中,这个过程称为rehash。

4、扩容

(1)扩容条件
为了保证访问效率,降低负载因子,常用的手段是扩容,当新元素将要添加进map时,会判断是否需要扩容。
触发扩容需要满足以下任一条件:

  • 平均负载因子大于6.5
  • overflow的数量达到2^min(15,B)

(2)增量扩容
当负载因子过大时,就新建一个bucket数组,新的bucket数组的长度为原来的2倍,然后旧bucket数组中的数据逐步搬迁到新的bucket数组中。

增量扩容的具体过程是这样的:

1、新建桶数组: 当触发增量扩容时,Go 会创建一个新的、更大的桶数组。

2、元素迁移: 然后,它会逐步将旧桶中的元素重新分配到新的桶数组中,避免一次性大规模的重新哈希。

3、渐进迁移: 在元素逐步迁移的过程中,新添加的元素会直接被放入新的桶数组中,而不会立即迁移。这保证了新元素的添加不会在迁移期间导致性能下降。

4、逐步替换: 最终,当所有元素都成功迁移到新的桶数组后,旧的桶数组会被废弃,新桶数组取而代之,完成了增量扩容的过程。

5、这种增量方式的扩容避免了在添加元素时出现大规模的哈希冲突或性能下降,因为它避免了在一次性扩容中发生的大量元素重新哈希的操作。这种方法相对于整体性地重新哈希整个 map 来说,更加有效和高效。

扩容后示意图:

在这里插入图片描述

搬迁完成后示意图:

在这里插入图片描述

5、增删改查

无论是元素的添加还是查询操作,都需要现根据键的Hash值确定一个bucket,并查询该bucket中是否存在指定的键。

  • 对于查询操作而言,查到指定的键后获取值后就返回,否则返回类型的空值。
  • 对于添加操作而言,查到指定的键意味着当前添加操作实际上是更新操作,否则在bucket中查找一个空余位置并插入。

(1)查找过程

查找过程简述如下:

  1. 计算 Hash 值: 对于给定的 key,通过哈希函数计算其对应的哈希值。
  2. 确定桶位置: 将计算得到的哈希值与当前 map 的桶数量 hmap.B 取模,以确定 key 应该放置在哪个桶中。这个桶就是存储相应 key-value 对的地方。
  3. 查找 TopHash: 从 tophash 数组中获取与当前桶位置对应的 tophash[i],其中 i 是 hash & (hmap.B - 1)。
  4. 比较 Hash 值: 如果 tophash[i] 中存储的哈希值与当前 key 的哈希值相等,那么表示可能找到了对应的桶,需要进一步检查。
  5. 比较实际值: 如果 tophash[i] 中存储的哈希值相等,接下来会比较实际的 key 值。如果找到了匹配的哈希值,但实际 key 不相等,这可能是碰撞,需要继续查找。
  6. 从桶中查找: 如果在当前桶中没有找到匹配的 key,就需要从溢出的桶中继续查找。溢出桶是因为哈希冲突导致多个 key 映射到同一个桶的情况。
  7. 返回结果: 如果找到匹配的 key,就返回对应的 value。如果遍历完所有相关的桶仍然没有找到匹配的 key,则返回相应类型的零值。

如果当前map处于搬迁过程中,则优先从oldbuckets数组中查找,查找到不再从新的buckets数组中查找。

(2)添加过程

新元素的添加过程简书如下:

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

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

(3)删除操作

删除元素实际上是先查找元素,如果元素存在则把元素从相应的bucket中清除,如果不存在则什么也不做

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

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

相关文章

2023年低压电工证考试题库及低压电工试题解析

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2023年低压电工证考试题库及低压电工试题解析是安全生产模拟考试一点通结合&#xff08;安监局&#xff09;特种作业人员操作证考试大纲和&#xff08;质检局&#xff09;特种设备作业人员上岗证考试大纲随机出的低压…

Postman接口Mock Servier服务器

近期在复习Postman的基础知识&#xff0c;在小破站上跟着百里老师系统复习了一遍&#xff0c;也做了一些笔记&#xff0c;希望可以给大家一点点启发。 应用场景&#xff1a;后端的接口还没有开发完成&#xff0c;前端的业务需要调用后端的接口&#xff0c;可以使用mock模拟。 一…

Java --- JVM的执行引擎

目录 一、执行引擎概述 1.1、执行引擎的工作过程 二、Java代码编译和执行的过程 三、解释器 3.1、解释器工作机制 3.2、解释器分类 3.3、解释器现状 四、JIT编译器 五、热点代码及探测方式 六、方法调用计数器 6.1、热点衰减 七、回边计数器 八、HotSpot VM设置程序…

【差旅游记】启程-新疆哈密(1)

哈喽&#xff0c;大家好&#xff0c;我是雷工。 最近有个新疆罗布泊的项目要去现场&#xff0c;领导安排我过去&#xff0c;这也算第一次到新疆&#xff0c;记录下去新疆的过程。 01、天有不测风云 本来预定的是11月2号石家庄飞成都&#xff0c;成都转机到哈密&#xff0c;但…

读书充电,温暖你的冬日,本期为大家送出几本架构师成长和软件架构技术相关的好书,助你度过这个不太景气的寒冬!

目图书录 ⭐️《高并发架构实战&#xff1a;从需求分析到系统设计》⭐️《架构师的自我修炼&#xff1a;技术、架构和未来》⭐️《中台架构与实现&#xff1a;基于DDD和微服务》⭐️《分布式系统架构&#xff1a;架构策略与难题求解》⭐️《流程自动化实战&#xff1a;系统架构…

Express基本接口开发-入门学习与后续进阶

前提推荐 任何一个新的知识都是从文档看起&#xff0c;因此express官方文档示例有必要去学习一遍。 推荐看&#xff1a; 推荐入门指南-路由指南-中间件 看完这几个内容之后心里大概知道express有些什么东西了&#xff0c;然后现在就可以去练习了 注意&#xff1a;更多示例-代…

csrf学习笔记总结

跨站请求伪造csrf csrf概述 掌握CSRF 漏洞原理 掌握CSRF 漏洞场景 掌握CSRF 漏洞验证 csrf原理 ​ 跨站请求伪造&#xff08;Cross Site Request Forgery&#xff0c;CSRF&#xff09;是一种攻击&#xff0c;它强制浏览器客户端用户在当前对其进行身份验证后的Web 应用程…

web 渗透 信息搜集

一 收集域名信息 1.whois查询 whois&#xff08;读作“Who is”&#xff0c;非缩写&#xff09;&#xff0c;标准的互联网协议&#xff0c…

开发板上网详细教程

开发板上网详细教程 PC端操作开发板操作 写在前面 今天想配置开发板的boa服务器&#xff0c;需要下载sudo apt-get install bison flex&#xff0c;但是一直报错&#xff0c;就蒙蔽了&#xff0c;后来想想真不应该啊&#xff0c;电脑和开发板通信没问题&#xff0c;但也只是如此…

解密网络世界的秘密——Wireshark Mac/Win中文版网络抓包工具

在当今数字化时代&#xff0c;网络已经成为了人们生活和工作中不可或缺的一部分。然而&#xff0c;对于网络安全和性能的监控和分析却是一项重要而又复杂的任务。为了帮助用户更好地理解和解决网络中的问题&#xff0c;Wireshark作为一款强大的网络抓包工具&#xff0c;应运而生…

正版软件|Ashampoo WinOptimizer 26 - Win优化器

使用 Ashampoo WinOptimizer 加速、优化和清洁你的电脑&#xff0c;非常轻松&#xff01; 关于Ashampoo WinOptimizer Windows是很棒&#xff0c;但总有改进的余地。 这就是Ashampoo WinOptimizer 26的用武之地! 因为&#xff0c;随着时间的推移&#xff0c;操作系统往往会变慢…

数据结构--图解单链表

学习链表最重要的就是会画图&#xff0c;尤其是要理解链表的逻辑结构和物理结构&#xff0c;理解链表的底层原理才能使用的如鱼得水。 希望这篇文章可以帮助各位&#xff0c;记得关注收藏哦&#xff1b;若发现问题希望私信博主&#xff0c;十分感谢。 当然学习链表是需要大家对…