#### go map 底层结构(详细) ####

部分内容摘自 再也不怕面试官拷打Go数据结构了!-Go语言map详解_go 语言的map底层数据结构-CSDN博客

首先了解下哈希冲突

解决方式有两种:拉链法或者开放寻址法。

拉链法:将数组中的元素换成指针,数组中的每个元素指向一个链表。遇到哈希冲突的情况就将冲突的元素连接到链表后面。拉点发处理冲突简单,但是当链表长度过长时,可以采用优化策略,例如采用红黑树代替链表。

开放地址法:简单来说就是当某个位置冲突时,就继续往后寻找空位,直到找到未使用的数据槽,将数据放入。

Go语言中map的底层结构

概括

1.1、map也即hmap(左图)。

1.2、hmap中有个字段是buckets(左图红框及下图),数组类型。

1.3、buckets数组的每个元素(下图黄框及右图)。

1.4、每个元素内有:

        1.4.1、字节数组字段(右图上方红框),存储map中的key和value,详细结构如下,注意并不是key0/value0/key1/value1的形式,这样做的好处是在key和value的长度不同的时候,可以消除padding带来的空间浪费:

        1.4.2、ovf_bucket_[n]字段(右图下方红框),hash冲突后拉链在此,形成链表结构。

详细

在Go语言中,map的底层结构是一个指向hmap的指针,占用8个字节。hmap中包含多个元素结构为bmap的bucket数组。bucket的底层采用链表将这些bmap连接起来。这里处理冲突用到了优化的拉链法,链表中的每个节点存储的不是一个键值对,而是8个键值对

map的数据结构在源码结构中的关键字段如下,在src/runtime/map.go中:

 type hmap struct {
     count     int     // 元素的个数
     flags     uint8   // 状态标志位,标记map的一些状态
     B         uint8   // buckets 数组的长度就是 2^B 个
     noverflow  uint16  // 溢出桶的数量 
     
     buckets    unsafe.Pointer  // 2^B个桶对应的数组指针,buckets数组的元素是bmap
     oldbuckets unsafe.Pointer  // 发生扩容时,记录扩容前的buckets数组指针
     
     extra *mapextra // 用于保存溢出桶的地址
 }
 
 type mapextra struct {
     overflow    *[]*bmap  // 溢出桶链表地址
     oldoverflow *[]*bmap  // 老的溢出桶链表地址
 
     nextOverflow *bmap  // 下一个空闲溢出桶地址
 }
 
 type bmap struct {
     tophash   [8]uint8  // 存储了bmap中8个k/v键值对的每个key根据哈希函数计算得出的hash值的高8位
     keys      [8]keytype // 存储了bmap中8个k/v键值对中的key
     values    [8]valuetype  // 存储了bmap中8个k/v键值对中的key
     overflow  uintptr // 指向溢出桶的指针
 }
 

map的访问步骤

  1. 判断map是否为空或者无数据,若为空或者无数据返回对应的空值
  2. map写检测,若正处于写状态,表示此时不能进行操作,报fatal error
  3. 计算hash值和掩码
  4. 通过最后的B位确定在哪号桶
  5. 根据key对应的前8位快速确定是在这个桶的哪个位置
  6. 对比key完成的hash是否匹配,匹配则获取对应value
  7. 如果都没有找到,就去连接的下一个溢出桶去找

这里也可以看出,tophash的作用是快速确定key是否正确,也可以理解成一种缓存措施,如果前8位都不对,那后面也没必要比较了。

map的赋值步骤

  1. map写检测,如果当前正处于写状态,表示此时不能进行赋值。
  2. 计算hash值,将map置为写状态
  3. 通过key的hash值后B位确定是哪一个桶
  4. 遍历当前桶,通过key的tophash和hash值,防止key重复,然后找到第一个可以插入的位置,即空位置存储数据。
  5. 如果当前桶元素已满,则会通过overflow连接创建一个新的桶。
  6. 再次判断map的写状态
  7. 清除map的写状态

map的扩容

map在两种情况下会触发扩容

1、map的负载因子(当前map中,每个桶中的平均元素个数)已经超过6.5

正常情况下,如果没有溢出桶,每一个桶中最多有8个元素,当平均每个桶中的数据超过6.5个,那就意味着当前容量不足了,要扩容。

2、溢出桶的数量过多

当B < 15时,如果overflow的bucket数量超过2^B
当B >= 15时,overflow的bucket数量超过2^15
简单来讲,新加入的key的hash后B位都一样,使得个别桶一直在插入新数据,进而导致它的溢出桶链条越来越长,如此一来,map在操作数据时就会变得很慢。及时扩容,可以重排数据,使元素在桶中的位置更加平均。

对应的,有两种不同的扩容策略

1、双倍扩容(负载因子 > 6.5)
2、等量扩容(溢出桶过多)

## 双倍扩容

发生这种扩容是由于当前桶数组实在不够用了,这种扩容元素会重排,可能会发生桶迁移。

注意:扩的是buckets数组,例如长度5扩为20,扩的不是那个链表。

当Go的map长度增长到大于加载因子所需的map长度时,Go语言就会将产生一个新的bucket数组,然后把旧的bucket数组移到一个属性字段oldbucket中。注意并不是立刻把旧的数组中的元素转义到新的bucket当中,而是只有当访问到具体的某个bucket的时候,会把bucket中的数据转移到新的bucket中。

如下图所示,当扩容的时候,Go的map结构体中,会保存【旧数据】和【新生成的数组】。

上面部分代表旧的有数据的bucket,下面部分代表新生成的新的bucket。蓝色代表存有数据的bucket,橘黄色代表空的bucket。 扩容时map并不会立即把新数据做迁移,而是当访问原来旧bucket的数据的时候,才把旧数据做迁移。注意这里并不会直接删除旧的bucket,而是把原来的引用去掉,利用GC清除内存。

如下图,扩容前B=2,扩容后B=3,假设一元素key的hash值后三位为101,那么由前文可知,扩容前,由hash的后两位决定几号桶,即01所以元素在1号桶。在扩容发生后,由hash的后三位来决定在几号桶,即101,所以元素会迁移到5号桶。

等量扩容

由于map中不断put和delete key,桶中可能会出现很多断断续续的空位,这些空位导致连接的bmap很长,导致扫描时间变长。这种扩容实际上是一种整理,把后置位的数据整理到前面,这种情况下,元素会重排,但不会换桶。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值