为什么遍历 Go map 是无序的
有的小伙伴没留意过 Go map 输出顺序,以为它是稳定的有序的;有的小伙伴知道是无序的,但却不知道为什么?有的却理解错误?今天我们将通过本文,揭开 for range map 的 “神秘” 面纱,看看它内部实现到底是怎么样的,输出顺序到底是怎么样?
前言
|
|
假设运行这段代码,输出结果是按顺序?还是无序输出呢?
2019/04/03 23:27:29 k: 3, v: zshipu4
2019/04/03 23:27:29 k: 4, v: zshipu5
2019/04/03 23:27:29 k: 0, v: zshipu1
2019/04/03 23:27:29 k: 1, v: zshipu2
2019/04/03 23:27:29 k: 2, v: zshipu3
从输出结果上来讲,是非固定顺序输出的,也就是每次都不一样(标题也讲了)。但这是为什么呢?
首先建议你先自己想想原因。其次我在面试时听过一些说法。有人说因为是哈希的所以就是无(乱)序等等说法。当时我是有点 ???
这也是这篇文章出现的原因,希望大家可以一起研讨一下,理清这个问题 :)
看一下汇编
...
0x009b 00155 (main.go:11) LEAQ type.map[int32]string(SB), AX
0x00a2 00162 (main.go:11) PCDATA $2, $0
0x00a2 00162 (main.go:11) MOVQ AX, (SP)
0x00a6 00166 (main.go:11) PCDATA $2, $2
0x00a6 00166 (main.go:11) LEAQ ""..autotmp_3+24(SP), AX
0x00ab 00171 (main.go:11) PCDATA $2, $0
0x00ab 00171 (main.go:11) MOVQ AX, 8(SP)
0x00b0 00176 (main.go:11) PCDATA $2, $2
0x00b0 00176 (main.go:11) LEAQ ""..autotmp_2+72(SP), AX
0x00b5 00181 (main.go:11) PCDATA $2, $0
0x00b5 00181 (main.go:11) MOVQ AX, 16(SP)
0x00ba 00186 (main.go:11) CALL runtime.mapiterinit(SB)
0x00bf 00191 (main.go:11) JMP 207
0x00c1 00193 (main.go:11) PCDATA $2, $2
0x00c1 00193 (main.go:11) LEAQ ""..autotmp_2+72(SP), AX
0x00c6 00198 (main.go:11) PCDATA $2, $0
0x00c6 00198 (main.go:11) MOVQ AX, (SP)
0x00ca 00202 (main.go:11) CALL runtime.mapiternext(SB)
0x00cf 00207 (main.go:11) CMPQ ""..autotmp_2+72(SP), $0
0x00d5 00213 (main.go:11) JNE 193
...
我们大致看一下整体过程,重点处理 Go map 循环迭代的是两个 runtime 方法,如下:
- runtime.mapiterinit
- runtime.mapiternext
但你可能会想,明明用的是 for range 进行循环迭代,怎么出现了这两个函数,怎么回事?
看一下转换后
|
|
实际上编译器对于 slice 和 map 的循环迭代有不同的实现方式,并不是 for 一扔就完事了,还做了一些附加动作进行处理。而上述代码就是 for range map 在编译器展开后的伪实现
看一下源码
runtime.mapiterinit
|
|
通过对 mapiterinit 方法阅读,可得知其主要用途是在 map 进行遍历迭代时进行初始化动作。共有三个形参,用于读取当前哈希表的类型信息、当前哈希表的存储信息和当前遍历迭代的数据
为什么
咱们关注到源码中 fastrand 的部分,这个方法名,是不是迷之眼熟。没错,它是一个生成随机数的方法。再看看上下文:
|
|
在这段代码中,它生成了随机数。用于决定从哪里开始循环迭代。更具体的话就是根据随机数,选择一个桶位置作为起始点进行遍历迭代
因此每次重新 for range map,你见到的结果都是不一样的。那是因为它的起始位置根本就不固定!
runtime.mapiternext
|
|
在上小节中,咱们已经选定了起始桶的位置。接下来就是通过 mapiternext 进行具体的循环遍历动作。该方法主要涉及如下:
- 从已选定的桶中开始进行遍历,寻找桶中的下一个元素进行处理
- 如果桶已经遍历完,则对溢出桶
overflow buckets进行遍历处理
通过对本方法的阅读,可得知其对 buckets 的遍历规则以及对于扩容的一些处理(这不是本文重点。因此没有具体展开)
总结
在本文开始,咱们先提出核心讨论点:“为什么 Go map 遍历输出是不固定顺序?”。而通过这一番分析,原因也很简单明了。就是 for range map 在开始处理循环逻辑的时候,就做了随机播种…
你想问为什么要这么做?当然是官方有意为之,因为 Go 在早期(1.0)的时候,虽是稳定迭代的,但从结果来讲,其实是无法保证每个 Go 版本迭代遍历规则都是一样的。而这将会导致可移植性问题。因此,改之。也请不要依赖…
参考
- 原文作者:知识铺
- 原文链接:https://index.zshipu.com/go/posts/go/map/2019-04-07-why-map-no-order/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。
- 免责声明:本页面内容均来源于站内编辑发布,部分信息来源互联网,并不意味着本站赞同其观点或者证实其内容的真实性,如涉及版权等问题,请立即联系客服进行更改或删除,保证您的合法权益。转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。也可以邮件至 sblig@126.com