大家好,我是炸鱼。不知道有多少Go面试题和漏洞是和for循环有关的。今天周末认真看了下,发现redefiningforloopvariablesemantics。知名硬核大师RussCox表示,他一直在研究这个问题,并表示十年的经验表明,目前的语义非常昂贵。问题案例一:取地址符号在Go语言中,我们在写for语句的时候,有时跑分和猜分的结果不一致。比如下面第一种情况的代码:varall[]*Itemfor_,item:=rangeitems{all=append(all,&item)}这个代码有问题吗?变量all中的item变量存储的是什么?它是每个循环的项目值吗?实际上,在for循环中,每次都将同一个item保存在变量中,也就是上一次循环的item值。这是围棋面试中经常出现的问题。结合goroutine就更骚了。毕竟会出现乱序输出等问题。如果要解决这个问题,需要改写程序如下:varall[]*Itemfor_,item:=rangeitems{item:=itemall=append(all,&item)}重新声明一个itemvariableputfor循环的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中重新定义语言,所以rsc不能直接这样做。因此,将由用户根据每个包的go.mod文件中的go行更改语义来控制这种“破坏”。如果我们在Go1.30中将本文讨论的for循环改为迭代,那么go.mod文件中的go版本声明将是一个key。如下图:Go1.30以后每次都会迭代变量,而早期的Go版本每次都会迭代变量。这样就会在一定范围内解决上面提到的for循环问题。总结for循环中的变量问题一直是各大围棋考官最喜欢的话题。另外,在实际编写Go代码的过程中,确实会遇到这样的坑。虽然rsc希望首创go.mod文件,使用goversion语句修改语义(不允许增删)。这无疑为Go1兼容性保证打开了后门。如果实施,此更改将导致Go的前后版本语义不同。还不如成为go.mod文件的语义开关。这显然是一个很折腾的问题。文章持续更新中。可以微信搜索【脑补炸鱼】阅读。本文已收录在GitHubgithub.com/eddycjy/blog中。学习Go语言可以看Go学习地图和路线。欢迎星星提醒。推荐阅读Goonlyiferr!=nil?不对,把这些优雅的搬运姿势分享给你!Go错误处理的新思路?用左边的函数和表达式先睹为快,Go2Error的奋斗之路Go书系列Go语言入门系列:实践中的Go项目初探Go语言编程之旅:深入使用Go做项目Go语言设计理念:Go语言进阶之旅:深入了解源码
