Go源码版本1.13.8

前言

是的,我也是一个PHPer,对于我们PHPer转Gopher的银 ,一定有个困扰:Go语言里每次遍历Map输出元素的顺序并不一致,但是在PHP里却是稳定的。今天我们就来看看这个现象的原因。本篇文章主要从如下节点展开:

  • Go的Map遍历结果“无序”
    • 遍历Map的索引的起点是随机的
  • Go的Map本质上是“无序的”
    • 无序写入
      • 正常写入(非哈希冲突写入)
      • 哈希冲突写入
    • 扩容
      • 成倍扩容迫使元素顺序变化
      • 等量扩容

Go的Map遍历结果“无序”

现象:Go语言里每次遍历Map输出元素的顺序并不一致,但是在PHP里却是稳定的。

关于这个现象我就不过多赘述了,同时我相信大家应该都网上搜过相关的文章,这些文章大多都说明了原因:For ... Range ... 遍历Map的索引的起点是随机的,没错,就是下面这段代码。

但是呢,有没有再推测过Go的作者们这么做背后的真正原因是什么?个人觉着因为:

Go的Map本质上是“无序的”

Go的Map本质上是“无序的”,为什么这么说?

“无序”写入

1. 正常写入(非哈希冲突写入):是hash到某一个bucket上,而不是按buckets顺序写入。

虽然buckets是一块连续的内存,但是新写入的键值可能写到这个bucket:


也可能写到这个bucket:


2. 哈希冲突写入:如果存在hash冲突,会写到同一个bucket上。

可能写到这个位置:


极限情况,也可能写到这个位置:


更有可能写到溢出桶去:



所以,写数据时,并没有单独维护键值对的顺序。而PHP(version 5)语言通过一个全局链表维护了Map里元素的顺序。

扩容

Go的Map的扩容有两种:

  • 成倍扩容
  • 等量扩容

1. 成倍扩容迫使元素顺序变化

为了简化理解我们对「成倍扩容」的理解,我们假设如下条件:

mapmapbucketbmap结构

同时根据如上的假设,我们得到此map对应的结构图示如下:

什么时候触发成倍扩容?
  • map写操作时
  • (元素数量/bucket数量) > 6.5时

通过下面的代码分析可知:

14:14

成倍扩容的过程如下:

bucketsoldbucketsbucketsbucketsbucketbmapbucketbmapbucketbmap

过程如下图所示(标红部分为本次扩容的bucket):

15:15


同时,通过上面的分析我们可以得到:成倍扩容迫使元素顺序变化

2. 等量扩容

什么时候触发等量扩容?

答案见下面的代码:

等量扩容的目的?

同样,为了简化理解我们对「等量扩容」的理解,我们假设如下条件:

mapmapbucketbmap结构bucketbucketsbmapbucketbucketsbmapbmapbmapbmapbmap

同时根据如上的假设,我们得到此map对应的结构图示如下:

为了说明「等量扩容」的作用,我们继续假设:

8:820:2030:30

此时,得到此map对应的结构图示如下:

36:36
36:36

从上图可以看出:

bmapbmapbmap

同时,通过上面的分析我们可以得到:等量扩容并没有改变元素顺序

结语

通过上文的分析,我们可知Go的Map的特性:

  • 无序写入
  • 成倍扩容迫使元素顺序变化

所以可以说「Go的Map是无序的」。

其次,通过本文我们:

  • 再次回顾了Go的Map遍历结果“无序”的原因
  • 了解了Map的写入过程
  • 了解了Map的「成倍扩容」和「等量扩容」的设计与目的