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

Gofor循环有时真的很棘手,..

时间:2023-03-13 05:51:23 科技观察

大家好,我是炸鱼。不知道有多少Go面试题和漏洞是和for循环有关的。今天周末仔细看了下,发现重新定义for循环变量语义[1]。看来大家踩的是同一个坑。知名硬核老大RussCox表示,他一直在研究这个问题,称十年的经验表明,目前语义的成本非常高,所以我们要搬家看看能不能打破兼容原则。想想之前的Go模块,真怕他一口气推掉这座塔……问题案例1在Go语言中,我们写for语句的时候,有时候运算的结果和猜测的不一致。比如下面第一种情况的代码:varall[]*Itemfor_,item:=rangeitems{all=append(all,&item)}这个代码有问题吗?变量all中的item变量存储的是什么?就是每次循环的item值,每次都不一样吧?实际上,在for循环中,每次都将同一个item保存在变量中,也就是上一次循环的item值。这是围棋面试中经常出现的问题。结合goroutine就更骚了。毕竟会出现乱序执行等问题。如果要解决这个问题,需要改写程序如下:varall[]*Itemfor_,item:=rangeitems{item:=itemall=append(all,&item)}重新声明一个localvariableitemvariable,存放for循环的item变量,然后追加。Case2下面是第二种情况的代码:varprints[]func()for_,v:=range[]int{1,2,3}{prints=append(prints,func(){fmt.Println(v)})}for_,print:=rangeprints{print()}这个程序的输出是什么?它是否输出没有&地址字符的1、2、3?结果,程序一运行,输出就是3,3,3。为什么?题目重点之一:注意闭包函数。其实所有的闭包都打印同一个v,即输出3,原因就是for循环结束后,v的最终值被设置为3,仅此而已。.如果要达到预期的效果,还是要用到通用重赋值。改写后的代码如下:for_,v:=range[]int{1,2,3}{v:=vprints=append(prints,func(){fmt.Println(v)})}Increasev:=v语句,程序输出1,2,3。仔细看看你写过的Go项目,你熟悉吗?用这个改造方法,我赢了。尤其是Goroutine的写法,很多同学在这里翻车的可能性会更大。解决方案修复思路事实上,Go核心团队在内部和社区中讨论了很长时间,希望重新定义for循环的语义。要实现的目标是:使循环变量每次迭代而不是每次循环。解决方法是:在每个迭代变量x的每个循环体的开头,加上一个隐式重赋值,即x:=x,可以解决上面程序中隐藏的坑。和我们现在做的一样,只是我们自己手动添加。Go团队所做的是在编译器中隐式处理它。用户自己决定是很尴尬的,因为Go团队在Proposal:Go2transition[2]中明确禁止重新定义语言的语义,所以rsc不能直接这样做。所以rsc打算开一个新的坑,希望用户自己决定控制这个“破解”,方式是根据各个模块的go.mod文件中的go行(版本声明)来确定语义.例如,如果本文讨论的for循环是在Go1.30中将循环变量更改为迭代,那么go.mod文件中的go版本声明将是一个关键的开关。如下图所示:和上图的配置一样,Go1.30以后每次都会迭代变量,而更早的Go版本每次都会迭代变量,即Go版本的go.mod控制new的语义功能,不同的模块可能会有所不同。这样就会在一定范围内解决上面提到的for循环问题。总结一下for循环中的变量问题一直是各大围棋考官最喜欢的话题,在实际编写围棋代码的过程中也确实会遇到这样的坑。虽然rsc希望首创go.mod文件,使用goversion语句修改语义(不允许增删)。这无疑为Go1兼容性保证打开了后门。如果实施,此更改将导致Go的前后版本语义不同。最好变成一个go.mod文件的语义开关,一改就改,否则这种改会给排错和理解带来很大的成本。这显然是一个很折腾的问题。