当前位置: 首页 > 科技观察

使用GoMap时要注意这个细节,避免依赖!

时间:2023-03-15 14:09:35 科技观察

本文转载自微信公众号《我的脑子是炸鱼》,作者陈建宇。转载本文请联系脑筋急转弯公众号。大家好,我是炸鱼。最近有同学问我这个日经话题,我想转发他的文章,结果发现我的公众号还没发,所以今天再唠叨几句,免得大家避而远之这个“坑”。有的朋友没有注意Gomap的输出和遍历顺序,认为它是稳定有序的,会直接依赖业务程序中结果集的顺序。结果,他们跌跌撞撞并遭受了在线错误的困扰。有的朋友知道是不对的,但是不知道为什么,有的理解错了?奇怪的输出结果今天通过这篇文章,我们将揭开forrangemap输出的“神秘面纱”,看看它的内部实现。怎么样,顺序是什么?开启吸鱼之路。序言示例如下:funcmain(){m:=make(map[int32]string)m[0]="EDDYCJY1"m[1]="EDDYCJY2"m[2]="EDDYCJY3"m[3]="EDDYCJY4"m[4]="EDDYCJY5"fork,v:=rangem{log.Printf("k:%v,v:%v",k,v)}}假设你运行这段代码,什么是输出结果?是有序输出还是无序输出?k:3,v:EDDYCJY4k:4,v:EDDYCJY5k:0,v:EDDYCJY1k:1,v:EDDYCJY2k:2,v:EDDYCJY3从输出结果来看,是不固定的输出是顺序的,即它每次都不一样。但这是为什么呢?首先,我建议你自己想想原因。其次,我在采访中听到了一些说法。有人说因为它是散列的,所以它没有(随机)顺序等等。那时候我有点???这也是本文的起因。希望大家一起讨论,把这个问题搞清楚:)看编译...0x009b00155(main.go:11)LEAQtype.map[int32]string(SB),AX0x00a200162(main.go:11)PCDATA$2,$00x00a200162(main.go:11)MOVQAX,(SP)0x00a600166(main.go:11)PCDATA$2,$20x00a600166(main.go:11)LEAQ“”..autotmp_3+24(SP),AX0x00ab00171(main.去:11)PCDATA$2,$00x00ab00171(main.go:11)MOVQAX,8(SP)0x00b000176(main.go:11)PCDATA$2,$20x00b000176(main.go:11)LEAQ""..autotmp_2+72(SP),AX0x00b500181(main.go:11)PCDATA$2,$00x00b500181(main.go:11)MOVQAX,16(SP)0x00ba00186(main.go:11)CALLruntime.mapiterinit(SB)0x00bf00191(main.go:11)JMP2070x00c100193(main.go:11)PCDATA$2,$20x00c100193(main.go:11)LEAQ""..autotmp_2+72(SP),AX0x00c600198(main.go:11)PCDATA$2,$00x00c600198(main.去:11)MOVQAX,(SP)0x00ca00202(main.go:11)CALLruntime.mapiternext(SB)0x00cf00207(main.go:11)CMPQ“”。.autotmp_2+72(SP),$00x00d500213(main.go:11)JNE193...我们看一下整体的流程,重点关注处理Gomap循环迭代的两个runtime方法,如下:运行时.mapiterinitruntime.mapiternext但是你可能会想,明明是使用forrangefor循环迭代,怎么会出现这两个函数,这是怎么回事呢?看一下转换后的varhitermap_iteration_structformapiterinit(type,range,&hiter);hiter.key!=nil;mapiternext(&hiter){index_temp=*hiter.keyvalue_temp=*hiter.valindex=index_tempvalue=value_temporiginalbody}其实编译器有不同的实现方式对于slice和map的循环迭代。上面的代码是forrangemap经过编译器展开后的伪实现。看源码runtime.mapiterinitfuncmapiterinit(t*maptype,h*hmap,it*hiter){...it.t=tit.h=hit.B=h.Bit.buckets=h.bucketsift.bucket.kind&kindNoPointers!=0{h.createOverflow()it.overflow=h.extra.overflowit.oldoverflow=h.extra.oldoverflow}r:=uintptr(fastrand())ifh.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方法我们可以知道,它的主要作用是在遍历地图时对地图进行初始化。形参一共有三个,分别用来读取当前哈希表的类型信息、当前哈希表的存储信息、当前遍历迭代的数据。为什么要关注源码中的fastrand部分呢?这个方法名是不是很熟悉?.没错,就是一种生成随机数的方法。再看看上下文:...//decidewheretostartr:=uintptr(fastrand())ifh.B>31-bucketCntBits{r+=uintptr(fastrand())<<31}it.startBucket=r&bucketMask(h.B)it.offset=uint8(r>>h.B&(bucketCnt-1))//iteratorstateit.bucket=it.startBucket在这段代码中,它产生了随机数。用于决定从哪里开始循环迭代。更具体地说,根据随机数,选择一个桶位置作为遍历迭代的起点。因此,每次重新换范围图,看到的结果都会不同。那是因为它的起始位置根本就不是固定的!runtime.mapiternextfuncmapiternext(it*hiter){...for;i