其实这样的 key 我随便搬迁到哪个 bucket 都行,当然,还是要搬迁到上面裂变那张图中的两个 bucket 中去 。但这样做是有好处的,在后面讲 map 迭代的时候会再详细解释,暂时知道是这样分配的就行 。确定了要搬迁到的目标 bucket 后,搬迁操作就比较好进行了 。将源 key/value 值 copy 到目的地相应的位置 。设置 key 在原始 buckets 的 tophash 为 evacuatedX 或是 evacuatedY,表示已经搬迁到了新 map 的 x part 或是 y part 。新 map 的 tophash 则正常取 key 哈希值的高 8 位 。下面通过图来宏观地看一下扩容前后的变化 。扩容前,B = 2,共有 4 个 buckets,lowbits 表示 hash 值的低位 。假设我们不关注其他 buckets 情况,专注在 2 号 bucket 。并且假设 overflow 太多,触发了等量扩容(对应于前面的条件 2) 。
扩容完成后,overflow bucket 消失了,key 都集中到了一个 bucket,更为紧凑了,提高了查找的效率 。
假设触发了 2 倍的扩容,那么扩容完成后,老 buckets 中的 key 分裂到了 2 个 新的 bucket 。一个在 x part,一个在 y 的 part 。依据是 hash 的 lowbits 。新 map 中 0-3 称为 x part,4-7 称为 y part 。
注意,上面的两张图忽略了其他 buckets 的搬迁情况,表示所有的 bucket 都搬迁完毕后的情形 。实际上,我们知道,搬迁是一个“渐进”的过程,并不会一下子就全部搬迁完毕 。所以在搬迁过程中,oldbuckets 指针还会指向原来老的 []bmap,并且已经搬迁完毕的 key 的 tophash 值会是一个状态值,表示 key 的搬迁去向 。map 的遍历本来 map 的遍历过程比较简单:遍历所有的 bucket 以及它后面挂的 overflow bucket,然后挨个遍历 bucket 中的所有 cell 。每个 bucket 中包含 8 个 cell,从有 key 的 cell 中取出 key 和 value,这个过程就完成了 。但是,现实并没有这么简单 。还记得前面讲过的扩容过程吗?扩容过程不是一个原子的操作,它每次最多只搬运 2 个 bucket,所以如果触发了扩容操作,那么在很长时间里,map 的状态都是处于一个中间态:有些 bucket 已经搬迁到新家,而有些 bucket 还待在老地方 。因此,遍历如果发生在扩容的过程中,就会涉及到遍历新老 bucket 的过程,这是难点所在 。我先写一个简单的代码样例,假装不知道遍历过程具体调用的是什么函数:
执行命令:go tool compile -S main.go得到汇编命令 。这里就不逐行讲解了,可以去看之前的几篇文章,说得很详细 。关键的几行汇编代码如下:
这样,关于 map 迭代,底层的函数调用关系一目了然 。先是调用 mapiterinit 函数初始化迭代器,然后循环调用 mapiternext 函数进行 map 迭代 。
迭代器的结构体定义:
mapiterinit 就是对 hiter 结构体里的字段进行初始化赋值操作 。前面已经提到过,即使是对一个写死的 map 进行遍历,每次出来的结果也是无序的 。下面我们就可以近距离地观察他们的实现了 。
推荐阅读
- 洗衣机进水不停自动放水故障
- 文件解压操作步骤 压缩文件怎么解压
- 女人长期单身有哪些危害
- 女生微信头像隐藏着哪些秘密
- 怎样结束和自己讨厌的人对话
- 省钱又浪漫的求婚方式
- 蓝光眼镜真的能保护眼睛吗?
- 女人生二胎意味着什么
- 如何在家中养殖水仙
- 孩子最不喜欢的6种妈妈