go map 底层实现原理

本文深入解析Go语言中Map的底层实现,包括基本结构、初始化、写入与读取数据的流程,以及扩容和数据迁移机制。在初始化时,根据元素数量创建hmap和bmap结构,并分配桶。写入数据时,根据哈希值定位桶并存储键值对。读取数据时,同样依赖哈希值和tophash进行查找。当达到特定负载因子或溢出桶过多时,Map会进行扩容,翻倍或等量扩容后,数据需要迁移至新桶。迁移过程中,数据会被重新分布到新桶中。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

go map 底层实现原理

go map 基本结构

go 中 map 的实现原理 其核心时由 hmap 和 bmap 两个结构体实现。

在这里插入图片描述
在这里插入图片描述

初始化

// 初始化一个可容纳10个元素的map
info = make(map[string]string,10)
  • 一:创建一个hmap结构体对象
  • 二:生成一个哈希因子 hash0 并赋值到 hmap 对象中
  • 三:根据 hint=10, 并根据算法规则来创建B,当前B 为1
hint	B
0~8		0
9~13	1
14~26	2
...
  • 四:根据B去创建桶(bmap对象)并存放在buckets数组中,当前bmap的数量为2

    • ​ 当B<4时,根据B创建桶的个数规则为:2^B(标准桶)
    • ​ 当B>=4时,根据B创建桶的个数的规则为:2B+2(B-4) (标准桶+移除桶)

    注意:每个bmap 中可以存储8个键值对,当不够存储时需要使用溢出桶,并将当前 bmap 中的 overrflow 字段指向溢出桶的

写入数据

info["name"] = "tom"

在map中写入如数据时,内部执行流程如下:

  • 一:结合哈希因子和键 name 生产哈希值 011011100011111110111011010
  • 二:获取哈希值的后B位,并根据后B位的值来决定将此键值对放到哪个桶中(bmap)

将哈希值和桶掩码(B为1的二进制) 进行 & 运算,最终得到哈希值的后B位的值。假设当B为1时,其结果为0:

哈希值: 011011100011111110111011010

桶掩码: 0000000000000000000000001

结果: 0000000000000000000000000 = 0

  • 三:在上一步确定桶之后, 接下来就在桶中写入数据

获取哈希值的tophash(哈希值的前8位),将tophash、key、value 分别写入如到桶中的三个数组中。如果桶已满,则通过overflow 找到溢出桶,并在溢出桶中继续写入

注意:以后在桶中查找数据时,会基于tophash来查找(tophash 相同则再去比较key)

  • 四:hmap的个数count++(map中元素个数+1 也是len() 函数放回的值 )

读取数据

value := info["name"]

在map中读取数据时,内部的执行流程为:

  • 一:结合哈希因子和键 name 生产哈希值
  • 二:获取哈希值的后B位,并根据后B位的值来决定将此键值对放到哪个桶中(bmap)
  • 三:确定桶之后, 再根据key的哈希值计算出 tophash , 根据 tophash 和 key 却去桶中查找数据。

当前桶中没找到,则根据 overflow 再去溢出桶中找,均未找到则表示key不存在

map 扩容

在向 map 中添加数据,达到某个条件, 则会引发字典扩容。

扩容条件:

  • map 中数据总个数/桶个数 > 6.5, 引起翻倍扩容
  • 使用了太多的溢出桶时(溢出桶使用太多会导致map处理速度降低)
    • B <= 15,已使用的溢出桶个数 >= 2 ^B 时, 引发等量扩容
    • B > 15, 已使用的溢出桶格式胡>=2^15 时,引发等量扩容
//hashGrow函数在runtime/map.go里
func hashGrow(t *maptype, h *hmap) {
	// B+1 相当于是原来 2 倍的空间
	bigger := uint8(1)

	// 对应条件 2
	if !overLoadFactor(int64(h.count), h.B) {
		// 进行等量的内存扩容,所以 B 不变
		bigger = 0
		h.flags |= sameSizeGrow
	}
	// 将老 buckets 挂到 buckets 上
	oldbuckets := h.buckets
	// 申请新的 buckets 空间
	newbuckets, nextOverflow := makeBucketArray(t, h.B+bigger)

	flags := h.flags &^ (iterator | oldIterator)
	if h.flags&iterator != 0 {
		flags |= oldIterator
	}
	// 提交 grow 的动作
	h.B += bigger
	h.flags = flags
	h.oldbuckets = oldbuckets
	h.buckets = newbuckets
	// 搬迁进度为 0
	h.nevacuate = 0
	// overflow buckets 数为 0
	h.noverflow = 0

	// ……
}

当扩容之后:

  • 一:B 会根据扩容后新桶的个数进行增加(翻倍扩容 B=旧B+1, 等量扩容 B = 旧B )
  • 二:oldbuckets 指向原来的桶(旧桶)
  • 三:buckets 指向新创建的桶(新桶 暂时没有数据)
  • 四:nevacuate 设置为0,表示如果数据迁移的话,应该从原桶中对的第0个位置开始迁移
  • 五:noverflow 设置为0 ,扩容后新桶中已使用的溢出桶为0
  • 六:extra.oldoverflow 设置为旧桶 已使用的所有溢出桶。即: h.extra.oldoverflow = h.extra.overflow
  • 七:extrra.overflow 设置为 null, 因为新桶中还未使用溢出桶
  • 八:extra.nextOverflow 设置为新创建的桶中的第一个溢出桶的位置

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DBW4eaOg-1666605607825)(go map 底层实现原理.assets/image-20221024172548744.png)]

map 迁移

扩容之后,必然要伴随着数据的迁移

翻倍扩容

如果是翻倍扩容,那么迁移就是将旧桶中的数据分流至新的两个桶中(比例不定),并且桶编号的位置为: 同编号位置 和 翻倍后对应的编号位置
在这里插入图片描述

迁移时会遍历某个旧桶中所有的key(包括溢出桶),并根据key重新生成哈希值,根据哈希值的 后B位来决定将此键值对分流到那个新桶中。
在这里插入图片描述

扩容后,B的值在原来的基础上已加1,也就意味着通过多1位来计算此键值对要分流到新桶的位置,如上图:

  • 当新增位(红色) 的值位 0, 则数据会迁移到与旧桶编号一致的位置
  • 当新增位(红色) 的值位 1, 则数据会迁移到翻倍后对应编号的位置

例如:

旧桶个数位32,翻倍后新桶个数位64

在重新计算旧桶中的所有key哈希值时,红色位只能是0或1,所以桶中的所有数据的后B 位只能是以下两种情况:

  • 000111 【】, 意味着要迁移到与旧桶编号一致的位置
  • 100111 【39】,意味着要迁移到翻倍后对应编号的位置

注意: 同一个桶中key的哈希值的低B位一定时相同的,不然不会放到同一个桶中,所以同一个桶中黄色标记的位都是相同的。

等量扩容

等量扩容的数据迁移机制比较简单,就是将旧桶中的值迁移到新桶中

这种扩容和迁移的意义在于:当溢出桶比较多而每个桶种的数据又不多时,可以通过等量扩容和迁移让数据更紧凑

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

go&Python

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值