一、map 基本操作(初始化+增删改查)

二、map底层数据结构

2.1 宏观上分析

golang的map 基于hashmap 实现的,底层用拉链法解决地址冲突。查找key时,先定位桶的位置,然后在桶内寻找。桶内找不到,再去溢出桶内寻找。

2.2 微观上分析

map有2个核心数据结构 hmap与bmap:

其中 mapextra用来存储溢出桶的信息。

bmap
hmap.bucketsbmaphmap.extra.overflowbmap

三、扩容

扩容有2个目的:原来的桶中数据越插越多,碰撞率增加;原来的桶越删越少,命中率变低(这就像是一座空城,房子很多,但是住户很少,都分散了,找起人来很困难)。

就有3种判断标准:

  1. 装载因子超过阈值,源码里定义的阈值是 6.5。其中loadFactor := count / (2^B),count 就是 map 的元素个数,2^B 表示 bucket 数量。
  2. overflow 的 bucket 数量过多。
  3. overflow和bucket中空余槽位过多。

对于条件 1,元素太多,而 bucket 数量太少,很简单:将 B 加 1,bucket 最大数量(2^B)直接变成原来 bucket 数量的 2 倍。于是,就有新老 bucket 了。注意,这时候元素都在老 bucket 里,还没迁移到新的 bucket 来。而且,新 bucket 只是最大数量变为原来最大数量(2^B)的 2 倍(2^B * 2)。

对于条件 3,其实元素没那么多,但是 overflow bucket 数特别多,说明很多 bucket 都没装满。解决办法就是开辟一个新 bucket 空间,将老 bucket 中的元素移动到新 bucket,使得同一个 bucket 中的 key 排列地更紧密。这样,原来,在 overflow bucket 中的 key 可以移动到 bucket 中来。结果是节省空间,提高 bucket 利用率,map 的查找和插入效率自然就会提升。

搬迁的时候老的 buckets 挂到了 oldbuckets 字段上,在此期间的查找会在原来的桶上进行。

对于条件1,map的序号会发生改变,导致map的遍历是无序的。这也是map不支持并发的原因,有并发需求可以尝试sync.map。

四、总结

1.通过 key 的哈希值将 key 散落到不同的桶中,每个桶中有 8 个 cell。哈希值的低位决定桶序号,高位标识同一个桶中的不同 key。

2.当向桶中添加了很多 key,造成元素过多,或者溢出桶太多,就会触发扩容。扩容分为等量扩容和 2 倍容量扩容。扩容后,原来一个 bucket 中的 key 一分为二,会被重新分配到两个桶中。