Go: For-Loop-Variable 适合面试的小问题

312 天前
 GopherDaily

在面试的过程中, 如果恰好遇到对方日常也使用 Go 做为主力语言, 我会选择一些简单而可扩展的问题交流下双方对 Go 的熟悉程度.

我喜欢的一个问题是让面试者告诉我下述代码的运行结果:

func main() {
	for i := 0; i < 3; i++ {
		go func() {
			fmt.Println(i)
		}()
	}

	time.Sleep(time.Second)
}

正确的答案应该是: 乱序输出三个数字. 对于三种错误答案: 输出 1, 2, 3; 输出三个数字; 乱序输出 1, 2, 3; 都可以通过反问再给予一次机会.

进一步的, 我们可以询问如何让其至少将 1, 2, 3 都输出一次. 大多数时候, 我们的得到的答案会是将 i 做为参数传入. 此时我喜欢再追问, 下述代码中 i := i 的写法是否正确.

func main() {
	for i := 0; i < 3; i++ {
		i := i
		go func() {
			fmt.Println(i)
		}()
	}

	time.Sleep(time.Second)
}

我并不认为这是一个 Language Lawyer 问题, 由于 Go 中 for 循环的特殊实现方式, i := i 这种方式在 Go 中是普遍存在的.

极少数情况下, 我们可以再讨论下上述例子的原因, 允许面试者有更大的发挥机会. 其中包括的点有:

我们在下述例子中看到, i 和 v 的内存地址始终未曾改变:

~ cat main.go
func main() {
    nums := []int{1, 2, 3}
    for i, v := range nums {
        fmt.Println(&i, &v)
    }
}
~ go run main.go
0x1400009a018 0x1400009a020
0x1400009a018 0x1400009a020
0x1400009a018 0x1400009a020
~ cat main.go | grep -A 7 "func fnVarScope"
func fnVarScope() {
    s := "hello world"
    {
        s := 10
        fmt.Println("s:", s)
    }
    fmt.Println("s:", s)
}
~ go run main.go
s: 10
s: hello world

Source: https://github.com/j2gg0s/j2gg0s/blob/main/_posts/2023-12-29-Go%3A%20For-Loop-Variable%20%E9%80%82%E5%90%88%E9%9D%A2%E8%AF%95%E7%9A%84%E5%B0%8F%E9%97%AE%E9%A2%98.md

4722 次点击
所在节点    Go 编程语言
33 条回复
lesismal
312 天前
个人觉得研究这些细节挺好玩,但是卷到面试题里真挺烦的

像我们很多务实的人喜欢按简单正确的方式写,不喜欢语法上的茴字的 N 种写法的那些奇技淫巧,所以除了手误、正常情况下不会在写 for lopp i 里再写个 i:=i ,即使要临时变量复制也是 idx:=i 或者其他变量名。
所以当我看到这种面试题,即使能答对,但仍然要因为同名变量耽误那么一下自己再确认下是不是自己眼花会不会看错、甚至猜测你们是不是出题手滑写错了,正常人怎么会写 i:=i 这种不规范的代码,所以又要担心,万一是你们出题错了我答对了会不会反倒被你们判断为答错了。。

同名局部变量这么搞用来迷惑老实人,感觉是跟风 cpp ,多点实在,少整点这种垃圾题目,尤其还有国内 golang 大论坛、公众号,也搞这些带节奏,然后一堆脑残面试官拿去恶心同行,搞得行业面试风气都差得很

隔三岔五看到这类题目就觉得很烦,建议改改
lifanxi
312 天前
加问一个问题,想保证顺序输出 0,1,2 ,这个程序要怎么改写?
geelaw
312 天前
随便看了一下文档,正确答案不是“乱序输出三个数字”,而是“乱序输出三个数字或者程序在不知道什么时候崩溃”。

https://go.dev/ref/mem

> While programmers should write Go programs without data races, there are limitations to what a Go implementation can do in response to a data race. An implementation may always react to a data race by reporting the race and terminating the program. Otherwise, each read of a single-word-sized or sub-word-sized memory location must observe a value actually written to that location (perhaps by a concurrent executing goroutine) and not yet overwritten.

另外 i 和 v 的地址未曾改变不能证明任何事情,即使每次迭代的变量是新的,编译器也可以证明复用旧的内存位置没问题,于是优化之后会看到相同的地址。
0o0O0o0O0o
312 天前
GOEXPERIMENT=loopvar🥵
codehz
312 天前
我记得 go 某个版本改了循环的语义啊
你再去问是不是有点不对
changz
312 天前
麻烦更新下八股文再发
adoal
312 天前
谭浩强老师不会老去,只会退休
Maboroshii
312 天前
正确答案是 不要写这样未知又模棱两可的代码...
nagisaushio
312 天前
然而新版本要改了,你版本过时了
SingeeKing
312 天前
今天发出来是不是有点晚了…… Go1.21 已经通过 GOEXPERIMENT=loopvar 改变了语义,前几天的 1.22rc1 更是作为了默认行为

------

不过这个面试题可以用来确认他有没有跟随 Go 的最新进度🌚
SingeeKing
312 天前
另外我倒是觉得这个还是挺重要的,因为和大多的八股不同,Go 因为这个引起的血案不少,很多人不知道(或者知道了写代码时候也不会有意识)循环变量在新起 goroutine 时会复用地址而出问题

----

但是退一步,知道这个也不代表写的时候能意识到(特别是改之前别人写的代码的时候)……
GopherDaily
312 天前
@lifanxi 那就不能用 goroutine 异步,改成同步变动太大了
GopherDaily
312 天前
@SingeeKing
- 大规模普及 1.22 可能还是以年为单位的
- 对于旧版本过来的人,不会因为新版本没有这个问题了,就不了解
GopherDaily
312 天前
@geelaw 对,但如果是考这个点的话,我觉得不适合面试;大多数时候我觉得知道输出的结果就行
GopherDaily
312 天前
@geelaw i/v 的地址不曾改变不是编译器优化的结果,而是明确在 spec 里面的

> Variables declared by the init statement are re-used in each iteration.
Link: https://go.dev/ref/spec#For_clause
xiaxiaocao
312 天前
Go 是神奇的语言
const x = 8
var a byte = 1 << x / 2
var y = x
var b byte = 1 << y / 2
fmt.Println(a, b)
猜猜输出是什么
me1onsoda
312 天前
下头
happyxhw101
312 天前
首先 “正确的答案应该是: 乱序输出三个数字” 这个是不对的,
最有可能的情况是输出 3, 3, 3
timnottom
312 天前
第一个不应该输出 333 吗???
timnottom
312 天前
@timnottom #19

#18 说得对

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1004376

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX