Golang 的 map 使用哈希表作为底层实现,一个哈希表里可以有多个哈希节点,即 bucket,而每个 bucket 就保存了 map 中的一个或一组键值对。
1、map 数据结构
type hmap struct {
count int // 当前保存的元素个数
flags uint8
B uint8 // 指示bucket数组大小
noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
hash0 uint32 // hash seed
buckets unsafe.Pointer // bucket数组指针,数组的大小为2^B
oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated)
extra *mapextra // optional fields
}
2、bucket 数据结构
type bmap struct {
tophash [bucketCnt]uint8 // 存储哈希值的高8位
}
每个 bucket 可以存储 8 个键值对。tophash 是个长度为 8 的数组,哈希值相同的键(准确的说是哈希值低位相同的键)存入当前 bucket 时会将哈希值的高位存储在该数组中,以方便后续匹配。
3、哈希冲突
当有两个或以上数量的键被哈希到同一个 bucket 时,我们称这些键发生了冲突。
Go 使用链地址来解决冲突。由于每个 bucket 可以存放 8 个键值对,所以同一个 bucket 存放超过 8 个键值对时会在创建一个键值对,用类似链表的方式将 bucket 连接起来。
4、负载因子
负载因子用于衡量一个哈希表冲突情况,公式为:
负载因子 = 键数量 / bucket 数量
例如,对于一个 bucket 数量为 4,包含 4 个键值对的哈希表来说,这个哈希表的负载因子为 1。
哈希表需要将负载因子控制在合适大小范围,超过其阈值需要进行 rehash,即键值对重新组织:
- 负载因子过小,说明空间利用率低。
- 负载因子过大,说明冲突严重,存取率低。
每个哈希表的实现对负载因子容忍程度不同,比如 Redis 实现中负载因子大于 1 时就会触发 rehash,而 Go 则在负载因子达到 6.5 时才会触发 rehash,因为 Redis 的每个 bucket 只能存 1 个键值对,而 Go 的 bucket 可能存 8 个键值对,所以 Go 可以容忍更高的负载因子。
5、扩容
5.1、扩容的前提条件
为了保证访问效率,当新元素将要添加进 map 时,都会检查是否需要扩容,扩容实际上是以空间换时间的手段。触发扩容的条件有 2 个:
- 负载因子 > 6.5 时,即平均每个 bucket 存储的键值对达到 6.5 个。
- overflow 数量 > 2^15 时,即 overflow 数量超过 32768 时。
5.2、增量扩容
当负载因子过大时,就新建一个 bucket,新的 bucket 长度是原来的 2 倍,然后旧 bucket 数据搬迁到新的 bucket。
考虑到如果 map 存储了数以亿计的 key-value,一次性搬迁将会造成较大的延时,Go 采用逐步搬迁策略,即每次访问 map 时都会触发一次搬迁,每次搬迁 2 个键值对。
5.3、等量扩容
所谓等量扩容,实际上并不是扩大容量,bucket 数量不变,重新做一遍类似增量扩容的搬迁动作,把松散的键值对重新排列一次,以便 bucket 的使用率更高,进而保证更快的存取。在极端场景下,比如不断的增删,而键值对正好集中在一小部分的 bucket ,这样会造成 overflow 的 bucket 数量增多,但负载因子又不高,从而无法执行增量搬迁情况。
6、map 查找过程
- 根据 key 值算出哈希值。
- 取出哈希值低位与 hmap.B 取模确定 bucket 位置
- 取哈希值高位在 tophash 数组中查询
- 如果 tophash[i] 中存储值与哈希值相等,则去找到该 bucket 中的 key 值进行比较。
- 当前 bucket 没有找到,则继续从下一个 overflow 的 bucket 查找。
- 如果当前处于搬迁过程,则优先从 oldbuckets 查找
注:如果查找不到,也不会返回空值,而是返回相应类型的 0 值。
7、插入过程
- 根据 key 值算出哈希值。
- 取出哈希值低位与 hmap.B 取模确定 bucket 位置。
- 查找该 key 是否已经存在,如果存在则直接更新值。
- 如果没有找到 key,将 key 插入。