用 Go map 要注意这个细节,避免依赖他!

有的小伙伴没留意过 Go map 输出、遍历顺序,以为它是稳定的有序的,会在业务程序中直接依赖这个结果集顺序,结果栽了个大跟头,吃了线上 BUG。

有的小伙伴知道是无序的,但却不知道为什么,有的却理解错误?
在这里插入图片描述
今天通过本文,我们将揭开 for range map 输出的 “神秘” 面纱,看看它内部实现到底是怎么样的,顺序到底是怎么样?

前言

例子如下:

func main() {m := make(map[int32]string)m[0] = "EDDYCJY1"m[1] = "EDDYCJY2"m[2] = "EDDYCJY3"m[3] = "EDDYCJY4"m[4] = "EDDYCJY5"for k, v := range m {log.Printf("k: %v, v: %v", k, v)}
}

假设运行这段代码,输出的结果是怎么样?是有序,还是无序输出呢?

k: 3, v: EDDYCJY4
k: 4, v: EDDYCJY5
k: 0, v: EDDYCJY1
k: 1, v: EDDYCJY2
k: 2, v: EDDYCJY3

从输出结果上来讲,是非固定顺序输出的,也就是每次都不一样。但这是为什么呢?
首先建议你先自己想想原因。其次我在面试时听过一些说法。有人说因为是哈希的所以就是无(乱)序等等说法。当时我是有点 ???
这也是这篇文章出现的原因,希望大家可以一起研讨一下,理清这个问题 :)

看一下汇编

    ...0x009b 00155 (main.go:11)	LEAQ	type.map[int32]string(SB), AX0x00a2 00162 (main.go:11)	PCDATA	$2, $00x00a2 00162 (main.go:11)	MOVQ	AX, (SP)0x00a6 00166 (main.go:11)	PCDATA	$2, $20x00a6 00166 (main.go:11)	LEAQ	""..autotmp_3+24(SP), AX0x00ab 00171 (main.go:11)	PCDATA	$2, $00x00ab 00171 (main.go:11)	MOVQ	AX, 8(SP)0x00b0 00176 (main.go:11)	PCDATA	$2, $20x00b0 00176 (main.go:11)	LEAQ	""..autotmp_2+72(SP), AX0x00b5 00181 (main.go:11)	PCDATA	$2, $00x00b5 00181 (main.go:11)	MOVQ	AX, 16(SP)0x00ba 00186 (main.go:11)	CALL	runtime.mapiterinit(SB)0x00bf 00191 (main.go:11)	JMP	2070x00c1 00193 (main.go:11)	PCDATA	$2, $20x00c1 00193 (main.go:11)	LEAQ	""..autotmp_2+72(SP), AX0x00c6 00198 (main.go:11)	PCDATA	$2, $00x00c6 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), $00x00d5 00213 (main.go:11)	JNE	193...

我们大致看一下整体过程,重点处理 Go map 循环迭代的是两个 runtime 方法,如下:

  • runtime.mapiterinit
  • runtime.mapiternext

但你可能会想,明明用的是 for range 进行循环迭代,怎么出现了这两个函数,怎么回事?

看一下转换后

var hiter map_iteration_struct
for mapiterinit(type, range, &hiter); hiter.key != nil; mapiternext(&hiter) {index_temp = *hiter.keyvalue_temp = *hiter.valindex = index_tempvalue = value_temporiginal body
}

实际上编译器对于 slice 和 map 的循环迭代有不同的实现方式,并不是 for 一扔就完事了,还做了一些附加动作进行处理。而上述代码就是 for range map 在编译器展开后的伪实现

看一下源码

runtime.mapiterinit

func mapiterinit(t *maptype, h *hmap, it *hiter) {...it.t = tit.h = hit.B = h.Bit.buckets = h.bucketsif t.bucket.kind&kindNoPointers != 0 {h.createOverflow()it.overflow = h.extra.overflowit.oldoverflow = h.extra.oldoverflow}r := uintptr(fastrand())if h.B > 31-bucketCntBits {r += uintptr(fastrand()) << 31}it.startBucket = r & bucketMask(h.B)it.offset = uint8(r >> h.B & (bucketCnt - 1))it.bucket = it.startBucket...mapiternext(it)
}

通过对 mapiterinit 方法阅读,可得知其主要用途是在 map 进行遍历迭代时进行初始化动作。共有三个形参,用于读取当前哈希表的类型信息、当前哈希表的存储信息和当前遍历迭代的数据

为什么

咱们关注到源码中 fastrand 的部分,这个方法名,是不是迷之眼熟。没错,它是一个生成随机数的方法。再看看上下文:

...
// decide where to start
r := uintptr(fastrand())
if h.B > 31-bucketCntBits {r += uintptr(fastrand()) << 31
}
it.startBucket = r & bucketMask(h.B)
it.offset = uint8(r >> h.B & (bucketCnt - 1))// iterator state
it.bucket = it.startBucket

在这段代码中,它生成了随机数。用于决定从哪里开始循环迭代。更具体的话就是根据随机数,选择一个桶位置作为起始点进行遍历迭代
因此每次重新 for range map,你见到的结果都是不一样的。那是因为它的起始位置根本就不固定!

runtime.mapiternext

func mapiternext(it *hiter) {...for ; i < bucketCnt; i++ {...k := add(unsafe.Pointer(b), dataOffset+uintptr(offi)*uintptr(t.keysize))v := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+uintptr(offi)*uintptr(t.valuesize))...if (b.tophash[offi] != evacuatedX && b.tophash[offi] != evacuatedY) ||!(t.reflexivekey || alg.equal(k, k)) {...it.key = kit.value = v} else {rk, rv := mapaccessK(t, h, k)if rk == nil {continue // key has been deleted}it.key = rkit.value = rv}it.bucket = bucketif it.bptr != b {it.bptr = b}it.i = i + 1it.checkBucket = checkBucketreturn}b = b.overflow(t)i = 0goto next
}

在上小节中,咱们已经选定了起始桶的位置。接下来就是通过 mapiternext 进行具体的循环遍历动作。该方法主要涉及如下:

  • 从已选定的桶中开始进行遍历,寻找桶中的下一个元素进行处理
  • 如果桶已经遍历完,则对溢出桶 overflow buckets 进行遍历处理

通过对本方法的阅读,可得知其对 buckets 的遍历规则以及对于扩容的一些处理(这不是本文重点。因此没有具体展开)

总结

在本文开始,咱们先提出核心讨论点:“为什么 Go map 遍历输出是不固定顺序?”。
经过这一番分析,原因也很简单明了。就是 for range map 在开始处理循环逻辑的时候,就做了随机播种…
你想问为什么要这么做?
当然是官方有意为之,因为 Go 在早期(1.0)的时候,虽是稳定迭代的,但从结果来讲,其实是无法保证每个 Go 版本迭代遍历规则都是一样的。而这将会导致可移植性问题。
因此,改之。也请不要依赖…

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/6956.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

PADS 规则设置-导线不跟随器件-导线允许回路

1、PADS Layout中设置拖动器件时导线不跟着移动 2、PADS Router中设置走线允许回路

【隧道篇 / WAN优化】(7.4) ❀ 01. 启动WAN优化 ❀ FortiGate 防火墙

【简介】几乎所有的人都知道&#xff0c;防火墙自带的硬盘是用来保存日志&#xff0c;以方便在出现问题时能找到原因。但是很少的人知道&#xff0c;防火墙自带的硬盘其实还有另一个功能&#xff0c;那就是用于WAN优化。 防火墙自带的硬盘 在FortiGate防火墙A、B、C、D系列&…

【备战软考(嵌入式系统设计师)】04-嵌入式软件架构

嵌入式操作系统 嵌入式系统有以下特点&#xff1a; 要求编码体积小&#xff0c;能够在有限的存储空间内运行。 面向应用&#xff0c;可以进行裁剪和移植。 用于特定领域&#xff0c;可以支持多任务。 可靠性高&#xff0c;及时响应&#xff0c;无需人工干预独立运行。 实…

软件全套资料整理包获取-软件各阶段支撑文档

软件全套精华资料包清单部分文件列表&#xff1a; 工作安排任务书&#xff0c;可行性分析报告&#xff0c;立项申请审批表&#xff0c;产品需求规格说明书&#xff0c;需求调研计划&#xff0c;用户需求调查单&#xff0c;用户需求说明书&#xff0c;概要设计说明书&#xff0c…

动手写一个简单的Android 表格控件支持固定列

Android 动手写一个简洁版表格控件 简介 源码已放到 Github Gitee 作为在测绘地理信息行业中穿梭的打工人&#xff0c;遇到各种数据采集需求&#xff0c;既然有数据采集需求&#xff0c;那当然少不了数据展示功能&#xff0c;最常见的如表格方式展示。 当然&#xff0c;类似…

大模型时序预测初步调研20240506

AI预测相关目录 AI预测流程&#xff0c;包括ETL、算法策略、算法模型、模型评估、可视化等相关内容 最好有基础的python算法预测经验 EEMD策略及踩坑VMD-CNN-LSTM时序预测对双向LSTM等模型添加自注意力机制K折叠交叉验证optuna超参数优化框架多任务学习-模型融合策略Transform…

MySQL —— 表的基本操作

一、创建 1.语法 create table 表名称( 自定义变量1, 自定义变量2, 自定义变量3&#xff08;最后一个变量末尾不需要加任何标点符号&#xff09; )charset字符集 collate校验集 engine存储引擎; ps&#xff1a;若是不具体给字符集、校验集、储存引擎&#xff0c;则采用配置文件…

『跨端框架』Flutter环境搭建

『跨端框架』Flutter环境搭建 资源网站简介跨平台高性能发展历程跨平台框架的比较成功案例 环境搭建&#xff08;windows&#xff09;基础环境搭建Windows下的安卓环境搭建Mac下的安卓环境配置资源镜像JDKAndroid StudioFlutter SDK问题一问题二问题三修改项目中的Flutter版本 …

厂家自定义 Android Ant编译流程源码分析

0、Ant安装 Windows下安装Ant&#xff1a; ant 官网可下载 http://ant.apache.org ant 环境配置&#xff1a; 解压ant的包到本地目录。 在环境变量中设置ANT_HOME&#xff0c;值为你的安装目录。 把ANT_HOME/bin加到你系统环境的path。 Ubuntu下安装Ant&#xff1a; sudo apt…

visio studio 中.NET Core(.net8.0)框架和.net framewok 框架有什么区别?

更新vs到2022版本后&#xff0c;新建项目时就多出不少选项&#xff0c;这里来个大家分享下.NET Core&#xff08;.net8.0&#xff09;框架和.net framewok的区别 如下图&#xff0c;不带后缀的就是使用.NET Core框架&#xff0c;后续选项是.net8.0。 .net framewok框架选项&am…

从0到1:商场导览小程序开发笔记一

背景 购物中心与商场小程序&#xff1a;旨在提供便捷的购物、导航、活动报名、服务查询等功能&#xff0c;让用户更好地体验购物和享受服务。通过提供便捷的购物、信息查询和互动预约等功能&#xff0c;提升了商场的服务水平和用户体验&#xff0c;帮助商场与消费者建立更紧密…

YOLOv5入门(四)训练自己的目标检测模型

前言 通过前面几篇文章&#xff0c;已经完成数据集制作和环境配置&#xff08;服务器&#xff09;&#xff0c;接下来将继续实践如何开始训练自己数据集~ 往期回顾 YOLOv5入门&#xff08;一&#xff09;利用Labelimg标注自己数据集 YOLOv5入门&#xff08;二&#xff09;处…

Android 14 init进程解析

前言 当bootloader启动后&#xff0c;启动kernel&#xff0c;kernel启动完后&#xff0c;在用户空间启动init进程&#xff0c;再通过init进程&#xff0c;来读取init.rc中的相关配置&#xff0c;从而来启动其他相关进程以及其他操作。 init进程启动主要分为两个阶段&#xff1…

jsPDF + html2canvas + Vue3 + ts项目内,分页导出当前页面为PDF、A 页面内导出 B 页面的内容为PDF,隐藏导出按钮等多余元素

jsPDF html2canvas Vue3 ts Arco Design项目&#xff0c;分页导出当前页面为PDF、A 页面内导出 B 页面的内容为PDF&#xff0c;隐藏导出按钮等多余元素… 1.下载所需依赖 pnpm install --save html2canvaspnpm install --save jspdf引入依赖 <script setup lang"…

OpenCV 库来捕获和处理视频输入和相似度测量(73)

返回:OpenCV系列文章目录&#xff08;持续更新中......&#xff09; 上一篇:OpenCV的周期性噪声去除滤波器(70) 下一篇 :OpenCV系列文章目录&#xff08;持续更新中......&#xff09; ​ 目标 如今&#xff0c;拥有数字视频录制系统供您使用是很常见的。因此&#xff0c;您…

FreeBSD RISCV 在QEME中实践-网络配置

在前一篇文章中&#xff0c;我们一起进行了FreeBSD RISCV 在QEME中实践 现在&#xff0c;让我们配置好网络吧&#xff01; 先上结论&#xff1a;用默认配置启动即可&#xff0c;网络就加载好了&#xff0c;只是不能ping罢了。因为不能ping&#xff0c;以为网络没通&#xff0…

opencv图片的平移-------c++

图片平移 cv::Mat opencvTool::translateImage(const cv::Mat& img, int dx, int dy) {// 获取图像尺寸int rows img.rows;int cols img.cols;// 定义仿射变换矩阵cv::Mat M (cv::Mat_<float>(2, 3) << 1, 0, dx, 0, 1, dy);// 进行仿射变换cv::Mat dst;cv…

[附源码+视频教程]暗黑纪元H5手游_架设搭建_畅玩三网全通西方3D世界_带GM

本教程仅限学习使用&#xff0c;禁止商用&#xff0c;一切后果与本人无关&#xff0c;此声明具有法律效应&#xff01;&#xff01;&#xff01;&#xff01; 教程是本人亲自搭建成功的&#xff0c;绝对是完整可运行的&#xff0c;踩过的坑都给你们填上了 一. 演示视频 暗黑纪…

目标检测——水下垃圾数据集DeepTrash

引言 亲爱的读者们&#xff0c;您是否在寻找某个特定的数据集&#xff0c;用于研究或项目实践&#xff1f;欢迎您在评论区留言&#xff0c;或者通过公众号私信告诉我&#xff0c;您想要的数据集的类型主题。小编会竭尽全力为您寻找&#xff0c;并在找到后第一时间与您分享。 …

[图解]不变式的构造和化简

1 00:00:02,420 --> 00:00:03,380 下面这个&#xff0c;我们来看 2 00:00:03,390 --> 00:00:09,940 X→select&#xff08;Y&#xff09;&#xff0c;用Y这个条件来筛选 3 00:00:09,950 --> 00:00:11,340 之后得到的集合 4 00:00:12,400 --> 00:00:14,390 forAl…