在读这篇文章之前,你应当已经知道Golang中对map的多次遍历得到的序列是不同的,Golang的开发团队这样设计的初衷是不让开发者误认为多次遍历map一定会得到稳定的输出序列,从而依赖于这一特性进行开发,产生未知的bug。
When iterating over a map with a range loop, the iteration order is not specified and is not guaranteed to be the same from one iteration to the next. Since the release of Go 1.0, the runtime has randomized map iteration order. Programmers had begun to rely on the stable iteration order of early versions of Go, which varied between implementations, leading to portability bugs. If you require a stable iteration order you must maintain a separate data structure that specifies that order.
那么这种特性是怎么实现的呢?让我们看一下源码(已省略无关细节):
fastrandrrstartBucketoffsetmapiternext
在网上的很多博客、文章里面都说map的遍历是随机选一个起点然后开始遍历的,所以每次得到的序列都不一样,少数提到了遍历顺序的也都是说按照bucket和cell的顺序依次遍历。不知道你这时候有没有产生一个疑问:如果是按照bucket和cell的顺序遍历,那么如果起点相同,我们得到的序列一定就相同咯?
Talk is cheap, show me the code. 我写了下面这样一段代码:
注意到上面我并没有直接用fmt打印key值,因为fmt可能会对map进行排序。你可以在这里查看:
Maps are now printed in key-sorted order to ease testing. The ordering rules are:
· When applicable, nil compares low
· ints, floats, and strings order by <
· NaN compares less than non-NaN floats
· bool compares false before true
· Complex compares real, then imaginary
· Pointers compare by machine address
· Channel values compare by machine address
· Structs compare each field in turn
· Arrays compare each element in turn
· Interface values compare first by reflect.Type describing the concrete type and then by concrete value as described in the previous rules.
这是我某一次的运行结果:

为什么会这样呢?明明都是从2开始遍历,却得到了不同的遍历序列?
mapiternext
bucketibucketCntoffi := (i + it.offset) & (bucketCnt - 1)offi
拿我上面程序的后两行输出来举例子,第二行的情况可能是这样的(为了方便理解,我直接把key值放在cell中了):

2 7 0 1 3 4 5 6 8 9
而第三行的输出,可能是下面这样的情形:

这里offset为0,而0号cell是空的,所以输出的第一个key仍然是2,但这不代表起点是2所在的cell!这样,当我们访问Bucket 0的时候,就是从0号cell开始访问,于是得到的输出序列为:
2 7 8 9 0 1 3 4 5 6
如果这篇文章让你对Golang中map的理解又深刻了一些,点赞支持一下原创吧!
参考文章
本文使用 Zhihu On VSCode 创作并发布