Go中的Map实现机制

2023-11-13

Go中的Map实现机制

一、map的使用方式

  1. 初始化
func main() {
  // 初始化方式一 make
  m := make(map[string]interface{},10)
  
  // 初始化方式二 字面量初始化
  m2 := map[string]interface{}{}
}
  1. 增删改查
func mapCRUD() {
  m := make(map[string]string, 10)
  m["apple"] = "red"  // 添加
  m["apple"] = "yellow" // 修改
  delete(m["apple"]) // 删除
  v, exist := map["apple"]
  if exist {
    fmt.Println(""apple is here!)
  }
}
  1. 危险操作
    1. 并发读写
      1. map不是原子操作,这意味着多个协程同时操作map时有可能产生读写冲突,读写冲突会触发panic从而导致程序退出
      2. Go是在map的实现中增加了读写检测机制,一旦发现读写冲突立马触发panic
      3. 建议:一般map参数不要被多个goroutine同时调用;使用带锁的map实现,一般用sync/map就能直接制造想要的带锁的map实现
      4. 触发读写冲突的代码如下:
func TestConcurrent(t *testing.T) {
    var m = map[string]int{}
    t.Run("write", func(t *testing.T) {
      // 加上并行
      t.Parallel()
      for i := 0; i < 10000; i++ {
        m["a"] = 1
      }
    })
    t.Run("read", func(t *testing.T) {
      // 加上并行
      t.Parallel()
      for i := 0; i < 10000; i++ {
        _ = m["a"]
      }
    })
}
  1. 空map
  2. 小结
  3. 初始化map时推荐使用内置函数make(),并指定预估的容量
  4. 修改键值对时,需要先查询指定键是否存在,否则map将创建新的键值对
  5. 查询键值对时,最好检查键是否存在,,避免操作零值
  6. 避免并发读写map,如果需要并发读写,则可以使用额外的锁(互斥锁、读写锁),可可以考虑使用标准库sync包中的sync.Map

二、实现原理

  1. 数据结构
    1. map的数据结构
// map的基础数据结构
type hmap struct {
	count     int	 // map存储的元素对计数,len()函数返回此值,所以map的len()时间复杂度是O(1)
	flags     uint8  // 记录几个特殊的位标记,如当前是否有别的线程正在写map、当前是否为相同大小的增长(扩容/缩容?)
	B         uint8  // hash桶buckets的数量为2^B个
	noverflow uint16 // 溢出的桶的数量的近似值
	hash0     uint32 // hash种子

	buckets    unsafe.Pointer // 指向2^B个桶组成的数组的指针,数据存在这里
	oldbuckets unsafe.Pointer // 指向扩容前的旧buckets数组,只在map增长时有效
	nevacuate  uintptr        // 计数器,标示扩容后搬迁的进度

	extra *mapextra // 保存溢出桶的链表和未使用的溢出桶数组的首地址
}

img

  1. bucket的数据结构
// 桶的实现结构
type bmap struct {
	// tophash存储桶内每个key的hash值的高字节
	// tophash[0] < minTopHash表示桶的疏散状态
	// 当前版本bucketCnt的值是8,一个桶最多存储8个key-value对
	tophash [bucketCnt]uint8
	// 特别注意:
	// 实际分配内存时会申请一个更大的内存空间A,A的前8字节为bmap
	// 后面依次跟8个key、8个value、1个溢出指针
	// map的桶结构实际指的是内存空间A
  
  data []byte // key value数据:key/key/key/.../value/value/value
  overflow *bmap // 溢出bucket地址
}
  1. tophash是一个长度为8的整形数组,hash值相同的键(准确的说是hash低位相同的键)存入当前bucket时会将hash值的高位存储在该数组中,以便后续匹配。
    1. data区存放的是key-value数据,存放顺序是key/key/key/…/value/value/value,如此存放是为了节省字节对齐带来的空间浪费
    2. overflow指针指向的事下一个bucket,据此将所有冲突的键连接起来。
  2. hash冲突
    1. 当有两个或以上数量的键被“Hash”到同一个bucket时,我们称这些键发生了冲突。Go使用链地址法来解决键的冲突。由于每个bucket可以存放8个键值对,所以同一个bucket存放超过8个键值对时就会再创建一个bucket,用类似链表的方式将bucket连接起来

img

  1. 负载因子
    1. 负载因子=键值数/bucket数量
    2. 负载因子过小,说明空间利用率低
    3. 负载因子过大,说明说明冲突严重,存取效率低
    4. go的map在负载因子大于6.5时会出发rehash
  2. 扩容
    1. 扩容条件
      1. 负载因子大于6.5时,即平均每个bucket存储的键值对达到6.5个以上
      2. overflow的数量达到2min(15,B)时。
    2. 增量扩容
      1. 当负载因子过大时,就新建一个bucket数组,新建的bucket数组的长租是原来的两倍,然后旧的bucket数组中的数组搬迁到新的bucket数组中
      2. 扩容时的处理非常巧妙,先试让hmap的数据结构中的oldbuckets成员指向原buckets数组,然后申请新的buckets数组(长度为原来的两倍),并将数组指针保存到hmap数据结构的buckets成员中。这样就完成了新老buckets数组的交接,后续的迁移工作将是从oldbuckets数组中逐步搬迁键值对到新的buckets数组中。待oldbuckets数组中所有键值对搬迁完毕后,就可以安全地释放oldbuckets数组了。
    3. 等量扩容
      1. 所谓的等量扩容,并不是扩大容量,而是bucket数量不变,重新做一遍类似增量扩容的搬迁动作,把松散的键值对重新排列一次,以使bucket的使用率变高,进而保证更快的存取效率。
        1. 在极端场景下,比如经过大量的元素增删后,键值对刚好集中在一小部分bucket中,这样会造成溢出的bucket数量增多。
  3. 增删查改
    1. 实现机制
      1. 对于查询操作而言,查询指定的键后获取值并返回,否则返回类型的空值
      2. 对于添加操作而言,查到指定的键意味着当前的添加操作实际上是更新操作,否则在bucket中查找一个空余位置并插入(待了解具体的插入实现)
    2. 查找过程
      1. 根据key值计算Hash值
      2. 取Hash值低位与hmap.B取模来确定bucket的位置
      3. 取Hash值高位,在tophash数组中查询
      4. 如果tophash[i]中存储的hash值与当前key的hash值相等,则获取top[i]的key值进行比较
      5. 当前bucket中没有找到,则依次从溢出的bucket中查找
      6. 如果当前map处于搬迁过程中,那么查找时优先从oldbuckets数组中查找,不再从buckets数组中查找
    3. 添加过程
      1. 根据key值算出hash值
      2. 取hash值低位与hmap.B取模来确定bucket的位置
      3. 查找该key是否已经存在,如果存在则直接更新值
      4. 如果该key不存在,则从该bucket中寻找空余位置并插入
      5. 当前map处于搬迁过程中,那么新元素会直接添加到新的buckets数组中,但查找过程仍从oldbuckets数组中开始
    4. 删除过程
      1. 删除元素实际上先查找元素,如果元素存在则把元素从bucket中清除,如果不存在则什么也不做
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Go中的Map实现机制 的相关文章

随机推荐

  • 图论:Dijkstra算法——最详细的分析,图文并茂,一次看懂!

    文章目录 1 Dijkstra算法简介 2 算法实现范例 3 邻接矩阵 4 Dijkstra 算法的 C 描述 5 Dijkstra 算法的 Matlab 描述 6 温故知新 1 Dijkstra算法简介 背景 迪杰斯特拉算法 Dijkst
  • Java中double类型大小比较的五种方法

    文章目录 1 使用BigDecimal 2 使用包装类Double 3 在误差范围内运行相等 4 转换成字符串 5 使用doubleToLongBits 方法 在Java中 int类型数据的大小比较可以使用双等号 double类型则不能使用
  • 设置SSH密钥对的必要性

    SSH介绍 SSH 为 Secure Shell 的缩写 由 IETF 的网络小组 Network Working Group 所制定 SSH 为建立在应用层基础上的安全协议 SSH 是较可靠 专为远程登录会话和其他网络服务提供安全性的协议
  • golang加载双向认证加密的证书key文件

    证书的key是可以加密保存的 我们需要进行解密加载 func MyLoadX509KeyPair certFile keyFile password string tls Certificate error certPEMByte err
  • 开放API接口或URL链接给第三方使用

    使用场景 场景1 应用做到一定程度后 会有一些第三方用户或机构要对接部分的功能进入他们自己的应用 比如 要通过URL的方式提供一个含有加密视频文件的H5页面给第三方使用 实现思路 1 后台管理系统给不同的第三方开权限 分别提供AppID和A
  • 学习率衰减

    在训练深度神经网络时 通常会随着训练的进行降低学习率 这可以通过使用预定义的学习率计划或自适应学习率方法来完成 学习率表 学习率时间表旨在根据预先定义的时间表降低学习率 从而在训练过程中调整学习率 常见的学习率时间表包括基于时间的衰减 逐步
  • AES算法的CBC和ECB模式!——密钥加密

    一 AES算法 AES Advanced Encryption Standard 是一种对称密钥加密算法 AES算法采用分组密码的方式 将明文划分成固定长度的数据块 对每个数据块进行加密 最终得到密文 AES算法的密钥长度可以为128位 1
  • 利用Netty构建自定义协议的通信

    在复杂的网络世界中 各种应用之间通信需要依赖各种各样的协议 比如 HTTP Telnet FTP SMTP等等 在开发过程中 有时候我们需要构建一些适应自己业务的应用层协议 Netty作为一个非常优秀的网络通信框架 可以帮助我们完成自定义协
  • 【Linux + 安装库】Ubuntu18.4.0下安装gmp+ntl+crytpo++库的相关操作

    一 装Ubuntu 1 虚拟机下装好的ubuntu系统安装编译器 首先需要添加源 代码 sudo add apt repository ppa ubuntu toolchain r test 2 添加源之后 安装编译器 gcc安装 sudo
  • Spring Boot 学习研究笔记(十二)Dcoker 中部署SpringBoot jar包

    Linux Centos8 使用 DOCKER 部署JAR包 1 进入项目根目录 cd project 2 创建存放jar包的目录 mkdir springboot test 3 进入 project springboot test 编写D
  • 软件调试之堆和堆检查

    当用户启动一个程序时 系统会将程序文件从外部存储器 硬盘等 加载到内存中 当程序工作时 需要使用内存空间来放置代码和数据 在使用一段内存之前 程序需要以某种方式 API或库函数 发出申请 接受到申请的一方 内存管理器或C运行库 根据申请者的
  • 【Ansible自动化运维实战】使用ansible批量部署开机启动时为字符界面

    Ansible自动化运维实战 使用ansible批量部署开机启动时为字符界面 一 查看当前启动默认的引导目标 二 编辑playbook 三 测试playbook语法 四 运行playbook 五 测试结果 一 查看当前启动默认的引导目标 a
  • 【信息技术】【2018.01】射频功率放大器的行为建模与数字预失真

    本文为奥地利格拉茨技术大学的博士论文 共147页 数字无线发射机中的射频功率放大器 RF PA 是关系到能量消耗和信号质量的关键部件 特别是由于当今宽带多载波调制方法产生的信号峰均比很高 很难构建出能量效率高 满足标准严格线性要求的射频功率
  • 自己动手写basic解释器(一)

    自己动手写basic解释器 刺猬 http blog csdn net littlehedgehog 注 文章basic解释源码摘自梁肇新先生的 编程高手箴言 据他所说这个代码也是网上摘录的 源码解读参考 java编程艺术 java编程艺术
  • 禁止显示状态 错误 LNK1104 无法打开文件“boost_thread-vc142-mt-gd-x64-1_79.lib”

    系列文章目录 文章目录 系列文章目录 前言 一 问题原因 二 解决办法 1 更改vs2019项目配置 2 第二种方法 前言 别人写的工程用vs2019加载 报错如下 一个错误 LNK1104 无法打开文件 boost thread vc14
  • Qt的各版本直接下载地址

    此文章转载自 http blog csdn net piaopiaolanghua article details 53153363 1 http download qt io archive qt 2 http ftp vim org l
  • spark机器学习笔记:设计机器学习系统

    感想 这是一篇机器学习通俗的讲解 我觉得讲得蛮好 特别是我们在设计机器学习系统的时候该怎么做 不是只设计一个机器学习算法就完了 还有很多的事情要做 本文对数据预处理归纳的挺全的 因为从用户获取的数据 不能直接用于机器学习模型的 中间还需要经
  • 上/下拉电阻对GPIO的影响

    疯雨 版权所有 转载请注明 http blog csdn net u010346967 本人菜鸟一枚 有错误的话欢迎指正 1 什么是上 下拉电阻 上拉就是将不确定的信号通过一个电阻嵌位在高电平 电阻同时起限流作用 下拉同理 上拉是对器件注入
  • 【Redis】Redis 通用命令、键的过期策略

    文章目录 一 基础命令 SET 和 GET 二 全局命令 KEYS EXISTS DEL EXPIRE 和 TTL 经典面试题 Redis 中 key 的过期策略是怎么实现的 TYPE Redis 有许多种数据结构 但是这些数据结构的 ke
  • Go中的Map实现机制

    Go中的Map实现机制 一 map的使用方式 初始化 func main 初始化方式一 make m make map string interface 10 初始化方式二 字面量初始化 m2 map string interface 增删