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

news2024/10/7 4:37:12

有的小伙伴没留意过 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), 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 进行循环迭代,怎么出现了这两个函数,怎么回事?

看一下转换后

var hiter map_iteration_struct
for mapiterinit(type, range, &hiter); hiter.key != nil; mapiternext(&hiter) {
    index_temp = *hiter.key
    value_temp = *hiter.val
    index = index_temp
    value = value_temp
    original body
}

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

看一下源码

runtime.mapiterinit

func mapiterinit(t *maptype, h *hmap, it *hiter) {
	...
	it.t = t
	it.h = h
	it.B = h.B
	it.buckets = h.buckets
	if t.bucket.kind&kindNoPointers != 0 {
		h.createOverflow()
		it.overflow = h.extra.overflow
		it.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 = k
			it.value = v
		} else {
			rk, rv := mapaccessK(t, h, k)
			if rk == nil {
				continue // key has been deleted
			}
			it.key = rk
			it.value = rv
		}
		it.bucket = bucket
		if it.bptr != b {
			it.bptr = b
		}
		it.i = i + 1
		it.checkBucket = checkBucket
		return
	}
	b = b.overflow(t)
	i = 0
	goto next
}

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

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

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

总结

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

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

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

相关文章

Flutter笔记:Widgets Easier组件库 - 使用标签(Tag)

Flutter笔记 Widgets Easier组件库 - 使用标签&#xff08;Tag&#xff09; - 文章信息 - Author: 李俊才 (jcLee95) Visit me at CSDN: https://jclee95.blog.csdn.netMy WebSite&#xff1a;http://thispage.tech/Email: 291148484163.com. Shenzhen ChinaAddress of this …

滑动窗口 | 1652. 拆炸弹 |LeetCode

文章目录 题目介绍暴力(可以过力扣竟然。不愧是简单题)&#xff1a;滑动窗口 祝你天天开心 题目介绍 你有一个炸弹需要拆除&#xff0c;时间紧迫&#xff01;你的情报员会给你一个长度为 n 的 循环 数组 code 以及一个密钥 k 。 为了获得正确的密码&#xff0c;你需要替换掉每…

【嵌入式笔试题】进程线程笔试题

非常经典的笔试题。 1.进程&线程(16道) 1.1异步IO和同步IO区别? 答案:如果是同步IO,当一个IO操作执行时,应用程序必须等待,直到此IO执行完。 相反,异步IO操作在后台运行,IO操作和应用程序可以同时运行,提高系统性能,提 高IO流量。 解读:在同步文件IO中,线…

IntelliJ IDEA 2024 for Mac:Java开发者的强大助手

IntelliJ IDEA 2024 for Mac是Java开发者不可或缺的强大助手&#xff0c;它凭借卓越的性能和丰富的功能&#xff0c;赢得了广大开发者的青睐。 作为集成开发环境&#xff08;IDE&#xff09;的佼佼者&#xff0c;IDEA 2024提供了全面的代码编辑和智能提示功能。它不仅能实时分析…

视频素材哪个软件好用?8个短视频素材高清无水印

在今日这个视觉表现至关重要的时代&#xff0c;获取合适的视频素材成为制作任何类型视频内容的基石。从企业宣传片到社交媒体短视频&#xff0c;高质量的视频素材能够显著提升内容的吸引力和专业度。这里列出了一些全球顶尖的视频素材平台&#xff0c;每一个都能为您的视频项目…

高效转化,智能私信软件策略揭秘

在数字营销的浪潮中&#xff0c;智能私信软件策略正成为提升转化率的重要工具。这种软件以其个性化、自动化的特点&#xff0c;正在重新定义与客户的互动方式&#xff0c;让企业能够更加高效地吸引并留住潜在客户。 智能私信软件的核心在于其高度的定制化和人性化设计。通过大数…

CasaOS玩客云安装memos开源云笔记并实现随时随地远程记笔记

文章目录 前言1. 使用Docker部署memos2. 注册账号与简单操作演示3. 安装cpolar内网穿透4. 创建公网地址5. 创建固定公网地址 前言 本文主要介绍如何在CasaOS玩客云&#xff0c;使用Docker本地部署21.6K stars的热门开源云笔记服务memos&#xff0c;并结合cpolar内网穿透工具打…

Llama3-Tutorial之XTuner微调Llama3个人小助手

Llama3-Tutorial之XTuner微调Llama3个人小助手 使用XTuner微调llama3模型。 参考&#xff1a; https://github.com/SmartFlowAI/Llama3-Tutorial 1. web demo部署 参考上一节内容已经完成web demo部署&#xff0c;进行对话测试, 当前回答基于llama3官方发布的模型进行推理生成&…

HCIP-Datacom-ARST必选题库_BGP【道题】

1.关于summary automatic命令和BGP聚合的描述,错误的是? 该命令用于实现自动聚合,其优先级高于手动聚合 配置该命令后,BGP将按自然网段聚合路由 该命令用来使能对本地引入的路由进行自动聚合 配置该命令后,BGP只向对等体发送聚合后的路由 1.关于summary automatic命令和BGP聚…

基于51单片机的手动数字时钟设计

基于51单片机的手动数字时钟 &#xff08;仿真&#xff0b;程序&#xff09; 功能介绍 具体功能&#xff1a; 1.八位数码管显示时分秒&#xff0c;格式为XX-XX-XX&#xff1b; 2.六个按键控制时、分、秒的加减&#xff1b; 3.复位按键重新计时&#xff1b; ​演示视频&am…

文件加密软件排行榜前五:好用的文件加密软件推荐

后台有很多老板留言&#xff0c;说最近机密数据外泄的事情频发&#xff0c;让自己开始有了危机意识&#xff0c;想要提前针对企业安全问题采取措施&#xff0c;比方说选一款适合防泄密软件&#xff0c;但是不知道如何选择。 下面介绍几款软件&#xff0c;让大家了解一下市面上常…

Python中的函数定义(def)详解

Python中的函数定义&#xff08;def&#xff09;详解 在编程语言中&#xff0c;函数是组织代码的一种方式&#xff0c;它们可以帮助我们将复杂的程序拆分为简单、易管理的部分。在Python中&#xff0c;函数的定义使用def关键字。 什么是函数&#xff1f; 函数是一段完成特定…

腾讯云服务器 宝塔面板部署小程序和后台教程

文章目录 目录 文章目录 安装流程 小结 概要部署流程技术细节小结 概要 本次的部署准备了3个域名&#xff0c;都是从二级域名映射出3个三级域名&#xff0c;域名注册可以在3大互联网官网购买一个域名就行。并且备案审核这些比较花费时间一般需要15工作日 部署流程 宝塔面板的…

设计模式Java实现-工厂模式

✨这里是第七人格的博客✨小七&#xff0c;欢迎您的到来~✨ &#x1f345;系列专栏&#xff1a;设计模式&#x1f345; ✈️本篇内容: 工厂模式✈️ &#x1f371;本篇收录完整代码地址&#xff1a;https://gitee.com/diqirenge/design-pattern &#x1f371; 楔子 记得刚…

JVM之内存分配的详细解析

内存分配 两种方式 不分配内存的对象无法进行其他操作&#xff0c;JVM 为对象分配内存的过程&#xff1a;首先计算对象占用空间大小&#xff0c;接着在堆中划分一块内存给新对象 如果内存规整&#xff0c;使用指针碰撞&#xff08;Bump The Pointer&#xff09;。所有用过的内…

快速入门!学习鸿蒙App开发的终极指南!

鸿蒙&#xff08;HarmonyOS&#xff09;是华为推出的一款分布式操作系统&#xff0c;旨在为不同设备提供统一的操作体验。鸿蒙App开发可以让应用程序在多个设备上实现流畅运行。本文将介绍鸿蒙App开发的终极指南&#xff0c;帮助您快速入门。 开发环境搭建 鸿蒙App开发过程需要…

R语言Rstudio突然无法启动?如何解决

&#x1f3c6;本文收录于「Bug调优」专栏&#xff0c;主要记录项目实战过程中的Bug之前因后果及提供真实有效的解决方案&#xff0c;希望能够助你一臂之力&#xff0c;帮你早日登顶实现财富自由&#x1f680;&#xff1b;同时&#xff0c;欢迎大家关注&&收藏&&…

fiscobcos 3.x linux安装与java简单调用

所用环境 vmware 16 Pro centos7.6 jdk11.0.6 ideal 2022 1、安装fiscobcos # 创建操作目录 # cd ~ && mkdir -p fisco && cd fisco# 下载建链脚本 # curl -#LO https://github.com/FISCO-BCOS/FISCO-BCOS/releases/download/v3.6.0/build_chain.sh &a…

知识图谱在提升大语言模型性能中的应用:减少幻觉与增强推理的综述

幻觉现象指的是模型在生成文本时可能会产生一些听起来合理但实际上并不准确或相关的输出&#xff0c;这主要是由于模型在训练数据中存在知识盲区所致。 为了解决这一问题&#xff0c;研究人员采取了多种策略&#xff0c;其中包括利用知识图谱作为外部信息源。知识图谱通过将信息…

Kalign 3:大型数据集的多序列比对

之前一直用的是muscle&#xff0c;看到一个文章使用了Kalign&#xff0c;尝试一下吧 安装 wget -c https://github.com/TimoLassmann/kalign/archive/refs/tags/v3.4.0.tar.gz tar -zxvf v3.4.0.tar.gz cd kalign-3.4.0 mkdir build cd build cmake .. make make test su…