【Go学习】语言基础(三)map

196 阅读4分钟

map的底层原理

路径:$GOROOT/src/runtime/map.go

通过阅读源码可知:

  • 每个桶里最多装8个<key,value>对 image.png 那么当第9个落入当前bucket该怎么办呢?

再构建一个bucket,并通过overflow指针连接起来。这就是链表法。

  • 用哈希查找表,使用链表法解决冲突 image.png

image.png

image.png

一层层组成了多级链表。

  • 在编译器编译时,bmap的结构还会被处理成:
type bmap struct {
   topbits [8]uint8
   keys [8]keytype
   values [8]valuetype 
   pad uintptr
   overflow uintptr
}
  • 创建map的底层调用了makemap函数。 任务:

设置哈希种子hash0: image.png

计算B的大小: image.png

  • 思考:slice和map分别作为函数参数时的区别? 创建slice底层调用makeslice,它返回的是slice这个结构体;而makemap如上图所示返回的是hmap的指针。

key、map几连问

map用的是什么hash函数?

image.png

image.png 如果CPU支持aes,用aes,否则用memhash

key定位过程

函数:mapaccess image.png

  • key经过哈希计算后得到64bit的哈希值(以64位机器为例)
  • 哈希值后B(bucket数组长度的对数)位表示在哪个bucket中。 例如B=5,哈希后五位为00110,则在下标为6的bucket中
  • 哈希值前8位表示tophash值。 如10010111表示在bucket结构中寻找HOB hash值为151的key。
  • 双层循环:外层遍历bucket和overflow bucket,内层遍历单个bucket所有槽位。
map是协程安全的吗?

不是。以map赋值过程为例:

image.png

函数会检查map的标志为flags。若flag为1,说明有其他协程正在执行写操作,而assign本身也是写操作,并发写会抛出panic。

插入或修改key

函数:mapassign 双层循环:外层遍历bucket和overflow bucket,内层遍历单个bucket所有槽位。

  • 扩容是渐进的:如果当前正在扩容,必须保证当前bucket对应的老bucket已经全部迁移完成才能赋值
  • 赋值操作是何时进行的? image.png

image.png 实际上mapassign()返回的是value的地址,便于后续赋值

谈谈map的删除过程

前半部分和查找、插入类似

  • 检测是否存在并发写操作(flag=1)
  • 计算key的哈希,找到落入的bucket
  • 设置flag
  • 如果map在扩容,触发搬迁操作
  • 找到key的位置:两层循环:外层遍历bucket和overflow bucket,内层遍历单个bucket所有槽位。
  • 对key或value清零,count-1,对应tophash改为emptyOne
  • 若当前位置后续的槽位都是emptyRest,将当前的emptyOne改成emptyRest;继续检查前一个位置,如果是emptyOne就改为emptyRest 关于最后两步的描述详见源码: image.png image.png image.png
谈谈map的扩容过程

函数:hashGrow image.png

  • 由图:扩容触发的条件

    • 装载因子超过阈值
    • 有太多overflow的bucket
  • 阈值是多少呢?6.5 image.png (loadFactorDen是2)

  • bucket总数和overflow里面的bucket数的关系? image.png 15是一个界限,B小于15时,bucket总数超过2^B次方;大于15时这个极限停留在2^15,不再增长


以上只是一些零碎知识点,核心流程在growWork()和evacuate()

image.png

growWork()调用evacuate() image.png

evacuate()代码有点长,但可以总结如下:

  • 如果是装载因子超过阈值,新的bucket数量是之前的一倍 要重新计算key的哈希
  • 如果是overflow的bucket过多,新的bucket数量和原来相等 可按序号搬
谈谈map的遍历过程
  • 大致思路:双层循环:外层遍历bucket和overflow bucket,内层遍历单个bucket所有cell,从cell中取出key、value。
  • 注意事项:由于扩容机制的存在,如果遍历发生在扩容期间,会涉及遍历新老bucket的过程。而源码是这样处理的:

image.png 从it.startBucket(随机位置的it.offset号的cell开始遍历(随机位置),取出其中的key和value,直到又回到起点bucket。

这也解释了为什么Go语言中的map里的key是无序的(从1.0版本起)

如何比较两个map是否相等

满足三者之一即可:

  • 都为nil:map1==map2
  • 非空,长度相等,指向同一个实体对象
  • 相应的key指向的value深度相等:遍历map中的元素,判断是不是每个都等
可以对key或value取值吗

不能,因为要考虑到扩容导致地址失效的问题

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 6 天,点击查看活动详情