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

一个简单的For循环也会踩坑

时间:2023-03-15 19:22:22 科技观察

本文转载自微信公众号“crossoverJie”,作者crossoverJie。转载本文请联系跨界姐公众号。前言最近在实现某项业务时,需要读取数据,然后进行异步处理;在Go中实现自然是比较简单的,伪代码如下:list:=[]*Demo{{"a"},{"b"}}for_,v:=rangelist{gofunc(){fmt.Println("name="+v.Name)}()}typeDemostruct{Namestring}看似很简单的几行代码,其实并不符合我们的预期,而且是打印后输出最重要的是:name=bname=bisnotwhatweexpected:name=aname=b坑1因为写go的资历和知识还很浅,找了几个小时才发现这个bug;一开始以为是dataSource的问题,经历了好几轮的自我怀疑。总之先不展示过程,先看看怎么解决这个问题。首先,第一种方式是使用临时变量:list:=[]*Demo{{"a"},{"b"}}for_,v:=rangelist{temp:=vgofunc(){fmt.Println("name="+temp.Name)}()}这样可以正确输出,其实从这种写法我们也能看出问题的端倪。第一种不使用临时变量时,主协程很快运行完毕,此时打印的子协程可能没有运行;当它开始运行时,这里的v已经被赋予了最后一个值。所以这里打印的总是最后一个变量。使用临时变量会复制当前遍历的值,自然不会互相影响。当然,除了临时变量,也可以使用闭包。list:=[]*Demo{{"a"},{"b"}}for_,v:=rangelist{gofunc(temp*Demo){fmt.Println("name="+temp.Name)}(v)}通过闭包传递参数时,每个goroutine都会在自己的栈中保存一份参数,这样也可以区分。坑2类似于第二个坑:list2:=[]Demo{{"a"},{"b"}}varalist[]*Demofor_,test:=rangelist2{alist=append(alist,&test)}fmt。println(alist[0].Name,alist[1].Name)这段代码不符合我们的预期:bb但我们可以稍微修改一下:list2:=[]Demo{{"a"},{"b"}}varalist[]Demofor_,test:=rangelist2{fmt.Printf("addr=%p\n",&test)alist=append(alist,test)}fmt.Println(alist[0].Name,alist[1].Name)addr=0xc000010240addr=0xc000010240ab顺便打印了内存地址,其实从结果也能猜到原因;每次遍历打印出来的内存地址都是一样的,所以如果我们存储指针,本质上是存储同一个内存地址的内容,所以值都是一样的。而如果我们只存值,不存指针就不会有这个问题。但是如果你想使用指针怎么办?list2:=[]Demo{{"a"},{"b"}}varalist[]*Demofor_,test:=rangelist2{temp:=test//fmt.Printf("addr=%p\n",&test)alist=append(alist,&temp)}fmt.Println(alist[0].Name,alist[1].Name)也很简单,同理使用临时变量即可。从官方源码我们可以知道,forrange只是一个语法糖,本质上也是一个for循环;因为每次都遍历赋值同一个对象,就会出现这样的“乌龙”。defer的坑forloop+defer也是一个组合坑(虽然不推荐使用这种方式),先看一个例子://demo1funcmain(){a:=[]int{1,2,3}for_,v:=rangea{deferfmt.Println(v)}}//demo2funcmain(){a:=[]int{1,2,3}for_,v:=rangea{deferfunc(){fmt.Println(v)}()}}分别输出://demo1321//demo2333demo1结果很好理解,defer可以理解为将执行语句入栈,所以呈现的结果是先进后出的。在demo2中,由于是闭包,闭包持有变量v的引用,所以在执行最后的延迟执行时v已经被赋了最后一个值,所以打印出来的结果是一样的。解决方法和上面类似,传入参数即可:for_,v:=rangea{deferfunc(vint){fmt.Println(v)}(v)}这么详细的问题在日常开发中不太可能遇到,你最有可能遇到的事情就是面试,所以了解更多也无妨。综上所述,类似于第一种在for循环中调用goroutine的情况,我觉得IDE可以做一个提醒;比如IDEA包含了大部分认为可能的错误,期待goland的后续更新。但其实这些错误,官博都有提醒过。图片https://github.com/golang/go/wiki/CommonMistakes#using-reference-to-loop-iterator-variable只是大多数人可能没见过。经过这件事,我不得不花时间仔细阅读。