Golang — map的使用心得和底层原理

news2025/1/18 9:56:36

 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 删

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

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

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

相关文章

浅谈如何做好软件项目

如何做好软件项目,这是摆在软件实施团队每个人面前的关键问题。笔者在此提出一些浅见,供大家参考。欢迎在评论区交流! 目录 【摘要】 【正文】 一、树立正确的需求调研理念 二、谋定而后动的开发工作 三、大道至简的系统设计 四、专注项…

Yoast SEO Premium插件下载,提升您的网站SEO排名

在当今数字化时代,网站的搜索引擎优化(SEO)至关重要。它不仅影响着网站的可见度,更直接关系到您的在线业务成功与否。如果您正在寻找一个能够显著提升网站SEO表现的工具,Yoast SEO Premium插件将是您的理想选择。 为什…

OpenHarmony 实战开发——如何编译OpenHarmony自带APP

概述 OpenHarmony 的主干代码是开源社区的重要学习资源,对于想进行应用开发和熟悉 OpenHarmony 能力的同学主干代码是非常重要的资源,在主干代码的 applications 目录里聚集了很多原生的应用实现,那么如何编译这些代码就是我们这篇文章的主要…

springboot+vue+mybatis灵活就业服务平台+PPT+论文+讲解+售后

随着网络科技的不断发展以及人们经济水平的逐步提高,网络技术如今已成为人们生活中不可缺少的一部分,而微信小程序是通过计算机技术,针对用户需求开发与设计,该技术尤其在各行业领域发挥了巨大的作用,有效地促进了灵活…

draw.text((left, top - 15), text,font=font, fill=“green”)

这是一个Python PIL库中的方法,用于在图片上绘制文本。具体来说,它可以在指定的位置绘制指定的文本,并使用指定的字体、颜色等参数进行渲染。其中,left和top是文本绘制的左上角坐标,text是要绘制的文本内容&#xff0c…

【linux学习】多线程(1)

文章目录 线程的概念线程与进程 线程的用法线程的创建多线程 线程的等待线程锁死锁 线程的概念 在Linux中,线程(Thread)是程序执行流的最小单位,是进程中的一个实体,负责在程序中执行代码。线程本身不拥有系统资源&…

只需三步将Kimi接入微信公众号

今天我将手把手交大家如何把Kimi大模型接入微信公众号,创建属于你自己的公众号智能助理,让你的公众号具备智能对话、文件阅读、信息搜索等强大功能,同时提高用户互动率、减少人工客服压力等。 废话不多说,先来看看实际效果吧~ 一…

简约在线生成短网址系统源码 短链防红域名系统 带后台

简约在线生成短网址系统源码 短链防红域名系统 带后台 安装教程:访问 http://你的域名/install 进行安装 源码免费下载地址抄笔记 (chaobiji.cn)https://chaobiji.cn/

AI视频教程下载:用ChatGPT自动化各种工作任务

这是一门实用的无代码课程,旨在通过使用ChatGPT高级数据分析和代码解释器提高生产力。 通过让ChatGPT代码解释器创建程序来自动化单调的任务,提高您的计算机生产力。 这门课程专为那些渴望快速使用小型实用程序的人设计,不需要编程知识。相…

下水道井盖多分类检测定位

下水道井盖识别,多分类,使用yolov5训练,采用一部分开源数据集和自建数据集。python pytorch opencv 深度学习#人工智能#深度学习#目标检测

在另外一个页面,让另外一个页面弹框显示操作(调佣公共的弹框)

大概意思是,登录弹框在另外一个页面中,而当前页面不存在,在当前页面中判断如果token不存在,就弹框出登录的弹框 最后一行 window.location.href … 如果当前用户已登录,则执行后续操作(注意此处,可不要)

7.STL_string1.0(详细)

目录 1. 什么是STL 2. STL的版本 3. STL的六大组件 1. 为什么学习string类? 1.1 C语言中的字符串 2. 标准库中的string类 2.1 string类(了解) 2.2 string类的常用接口说明 1. string类对象的常见构造 2. string类对象的容量操作 reserve 3. string类对象…

省级生活垃圾无害化处理率面板数据(2004-2022年)

01、数据简介 生活垃圾无害化处理率是指经过处理的生活垃圾中,达到无害化标准的垃圾所占的比例。这一指标是衡量城市垃圾处理水平的重要标准,反映了城市对垃圾进行有效管理和处理的能力。 生活垃圾无害化处理的主要方式包括生活垃圾焚烧、生活垃圾卫生…

数据挖掘原理与应用------分类预测

在数据挖掘和机器学习领域,TPR(True Positive Rate)是指在实际为阳性的情况下,模型正确预测为阳性的比例。TPR也被称为灵敏度(Sensitivity)或召回率(Recall)。它是评估分类模型性能的…

构建教育新未来:智慧校园平台的深度解读与全景呈现

引言 在全球数字化转型的大潮中,智慧校园平台作为教育信息化的重要载体,正以前所未有的姿态颠覆传统的教育模式,引领教育行业步入一个崭新的时代。这个融合了大数据、人工智能、云计算、物联网等一系列前沿科技的平台,以其强大的功…

QT day2 作业

头文件 #ifndef MYWIDGET_H #define MYWIDGET_H#include <QWidget> #include <QDebug> #include<QIcon> #include<QLabel> #include<QMovie> #include<QLineEdit> #include<QPushButton> QT_BEGIN_NAMESPACE namespace Ui { class …

浅谈SiC MOSFET之MOSFET

1.掺杂后的半导体 P型半导体&#xff0c;多子是空穴&#xff0c;少子是自由电子。 N型半导体&#xff0c;多子是自由电子&#xff0c;少子是空穴。 2.电中性 尽管他们分别有着空穴带正电&#xff0c;自由电子带负电&#xff0c;但是整体上是电中性的。 以P型半导体为例&…

Leaflet.canvaslabel在Ajax异步请求时bindPopup无效的解决办法

目录 前言 一、场景重现 1、遇到问题的代码 2、问题排查 二、通过实验验证猜想 1、排查LayerGroup和FeatureGroup 2、排查Leaflet.canvaslabel.js 三、柳暗花明又一村 1、点聚类的办法 2、歪打正着 总结 前言 在上一篇博客中介绍了基于SpringBoot的全国风景区WebGIS按…

实验室纳新宣讲会(java后端)

前言 2024-5-12 22:00:39 这是陈旧已久的草稿 2021-09-16 15:41:38 发布一下 当时我进入实验室&#xff0c;也是大二了&#xff0c;实验室纳新需要宣讲&#xff0c; 但是当时有疫情&#xff0c;又没宣讲成。 实验室纳新宣讲会&#xff08;java后端&#xff09; 首先&#x…

重写muduo之TcpConnection

目录 1、 TcpConnection.h 2、 TcpConnection.cc 1、 TcpConnection.h TcpConnection底层绑定&#xff08;管理&#xff09;了一个Channel&#xff0c;Channel有事件被Poller通知后&#xff0c;会调用相应的回调&#xff0c;这些回调也是TcpConnection中包含的方法&#xff0c…