Golang Go语言中 For-Loop-Variable 适合面试的小问题

发布于 1周前 作者 yibo5220 来自 Go语言

在面试的过程中, 如果恰好遇到对方日常也使用 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 中是普遍存在的.

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

  • Go 并不保证先启动的 goroutine 先执行
  • Go 中 for 循环的实现是 one-instance-per-loop, 而不是 one-instance-per-iteration.

我们在下述例子中看到, 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
  • 闭包(closure)可能以值(by value)或者地址(by reference)的形式引用外部变量; 当引用 for 循环的中变量时, 是以地址的方式
  • Go 允许在 inner block 中定义重名的变量, 下述代码虽然不好但合法
~ 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


Golang Go语言中 For-Loop-Variable 适合面试的小问题

更多关于Golang Go语言中 For-Loop-Variable 适合面试的小问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html

34 回复

个人觉得研究这些细节挺好玩,但是卷到面试题里真挺烦的

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

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

隔三岔五看到这类题目就觉得很烦,建议改改

更多关于Golang Go语言中 For-Loop-Variable 适合面试的小问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


加问一个问题,想保证顺序输出 0,1,2 ,这个程序要怎么改写?

随便看了一下文档,正确答案不是“乱序输出三个数字”,而是“乱序输出三个数字或者程序在不知道什么时候崩溃”。

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 的地址未曾改变不能证明任何事情,即使每次迭代的变量是新的,编译器也可以证明复用旧的内存位置没问题,于是优化之后会看到相同的地址。

GOEXPERIMENT=loopvar🥵

我记得 go 某个版本改了循环的语义啊
你再去问是不是有点不对

麻烦更新下八股文再发

谭浩强老师不会老去,只会退休

正确答案是 不要写这样未知又模棱两可的代码…

然而新版本要改了,你版本过时了

今天发出来是不是有点晚了…… Go1.21 已经通过 GOEXPERIMENT=loopvar 改变了语义,前几天的 1.22rc1 更是作为了默认行为

------

不过这个面试题可以用来确认他有没有跟随 Go 的最新进度🌚

另外我倒是觉得这个还是挺重要的,因为和大多的八股不同,Go 因为这个引起的血案不少,很多人不知道(或者知道了写代码时候也不会有意识)循环变量在新起 goroutine 时会复用地址而出问题

----

但是退一步,知道这个也不代表写的时候能意识到(特别是改之前别人写的代码的时候)……

那就不能用 goroutine 异步,改成同步变动太大了


- 大规模普及 1.22 可能还是以年为单位的
- 对于旧版本过来的人,不会因为新版本没有这个问题了,就不了解

对,但如果是考这个点的话,我觉得不适合面试;大多数时候我觉得知道输出的结果就行

i/v 的地址不曾改变不是编译器优化的结果,而是明确在 spec 里面的

> Variables declared by the init statement are re-used in each iteration.
Link: https://go.dev/ref/spec#For_clause

Go 是神奇的语言
const x = 8
var a byte = 1 << x / 2
var y = x
var b byte = 1 << y / 2
fmt.Println(a, b)
猜猜输出是什么

首先 “正确的答案应该是: 乱序输出三个数字” 这个是不对的,
最有可能的情况是输出 3, 3, 3

第一个不应该输出 333 吗???

#19

#18 说得对

你这个 c++问 ub 有什么区别?


我靠,为啥是 0 呢

自己掌握的知识都是错的,还面试别人。面试者真的惨。

#14 错误的方式可以有很多,没有必然理由认为你的错误答案程度更轻,建议不要和别人玩“揣摩出题人意思”的游戏。

#15 你可能没有理解我的意思。

>我们在下述例子中看到, i 和 v 的内存地址始终未曾改变: …
>另外 i 和 v 的地址未曾改变不能证明任何事情,即使每次迭代的变量是新的,编译器也可以证明复用旧的内存位置没问题,于是优化之后会看到相同的地址。

引述第一段内容,阅读后揣摩得出:这段话是想要通过地址不改变 (A) 证明迭代变量每次都是同一个 (B),即论证 A => B ,并观察到 A ,因此推出 B 。
引述第二段内容,意思是 A => B 的论证不恰当,并不是说 A => B 这个命题本身不真。

换言之,A 对论证 B 是无意义的,论证 B 的惟一方式是阅读 Go 的定义。

我不会写 go 啊。但是按你的说法如果我已经把一个变量传进一个闭包了,为啥复用这个变量的内存位置没问题啊?

第一题不是 3 3 3 吗

这种官方狗屎设计导致的 bug ,有什么好面的,新版本里都要修复了

#25 你可能没有仔细阅读楼主的内容,请看第三段代码片段,那里面没有闭包。

嗷嗷 确实 那没毛病



var x = 8
var a byte = 1 << x / 2
这样 a 就是 0 ,难道是为了省空间,计算 1<<x 的时候直接在 a 上操作,导致溢出了??

如果是 const x = 8 的话,const 会在编译的时候计算完 1<<x/2 的结果,于是能够放进 byte 里不会溢出。

javascripts 应该是 3 3 3

这个面试者太惨了,

在Golang(Go语言)中,关于For-Loop(循环)变量的小问题非常适合用于面试,因为它们能考察候选人对Go语言特性和内存管理的理解。以下是一个适合面试的小问题及其解析:

问题: 在Go语言中,当你在for循环中使用短变量声明(例如 i := range)时,变量的作用域是什么?如果在循环体内再声明一个同名变量,会发生什么?

解析

  1. 作用域:在Go语言的for循环中,使用短变量声明(i, v := range someSlice)时,变量iv的作用域仅限于该for循环的迭代块内。这意味着在循环外部无法访问这些变量。

  2. 同名变量声明:如果在for循环的迭代块内部再声明一个与循环变量同名的变量(例如,使用var i inti := someValue),这会在迭代块内创建一个新的局部变量i,它会遮蔽(shadow)外部的循环变量i。需要注意的是,这种遮蔽可能会导致代码难以理解和维护,因此在实际开发中应尽量避免。

这个问题不仅考察了候选人对Go语言作用域规则的理解,还间接测试了他们对代码可读性和最佳实践的认识。通过这个问题,面试官可以评估候选人是否具备编写清晰、可维护Go代码的能力。

希望这个回答对你有所帮助!

回到顶部