V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
GopherDaily
V2EX  ›  Go 编程语言

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

  •  
  •   GopherDaily · 335 天前 · 4910 次点击
    这是一个创建于 335 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

    第 1 条附言  ·  335 天前
    一些扩展的文章
    - Fixing For Loops in Go 1.22 https://go.dev/blog/loopvar-preview
    - Proposal: Less Error-Prone Loop Variable Scoping https://go.googlesource.com/proposal/+/master/design/60078-loopvar.md
    33 条回复    2023-12-30 10:50:32 +08:00
    lesismal
        1
    lesismal  
       335 天前   ❤️ 10
    个人觉得研究这些细节挺好玩,但是卷到面试题里真挺烦的

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

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

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

    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
        4
    0o0O0o0O0o  
       335 天前 via iPhone
    GOEXPERIMENT=loopvar🥵
    codehz
        5
    codehz  
       335 天前 via iPhone
    我记得 go 某个版本改了循环的语义啊
    你再去问是不是有点不对
    changz
        6
    changz  
       335 天前 via Android
    麻烦更新下八股文再发
    adoal
        7
    adoal  
       335 天前
    谭浩强老师不会老去,只会退休
    Maboroshii
        8
    Maboroshii  
       335 天前
    正确答案是 不要写这样未知又模棱两可的代码...
    nagisaushio
        9
    nagisaushio  
       335 天前 via Android
    然而新版本要改了,你版本过时了
    SingeeKing
        10
    SingeeKing  
       335 天前
    今天发出来是不是有点晚了…… Go1.21 已经通过 GOEXPERIMENT=loopvar 改变了语义,前几天的 1.22rc1 更是作为了默认行为

    ------

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

    ----

    但是退一步,知道这个也不代表写的时候能意识到(特别是改之前别人写的代码的时候)……
    GopherDaily
        12
    GopherDaily  
    OP
       335 天前
    @lifanxi 那就不能用 goroutine 异步,改成同步变动太大了
    GopherDaily
        13
    GopherDaily  
    OP
       335 天前
    @SingeeKing
    - 大规模普及 1.22 可能还是以年为单位的
    - 对于旧版本过来的人,不会因为新版本没有这个问题了,就不了解
    GopherDaily
        14
    GopherDaily  
    OP
       335 天前
    @geelaw 对,但如果是考这个点的话,我觉得不适合面试;大多数时候我觉得知道输出的结果就行
    GopherDaily
        15
    GopherDaily  
    OP
       335 天前
    @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
        16
    xiaxiaocao  
       335 天前
    Go 是神奇的语言
    const x = 8
    var a byte = 1 << x / 2
    var y = x
    var b byte = 1 << y / 2
    fmt.Println(a, b)
    猜猜输出是什么
    me1onsoda
        17
    me1onsoda  
       335 天前
    下头
    happyxhw101
        18
    happyxhw101  
       335 天前
    首先 “正确的答案应该是: 乱序输出三个数字” 这个是不对的,
    最有可能的情况是输出 3, 3, 3
    timnottom
        19
    timnottom  
       335 天前
    第一个不应该输出 333 吗???
    timnottom
        20
    timnottom  
       335 天前
    @timnottom #19

    #18 说得对
    MoYi123
        21
    MoYi123  
       335 天前
    你这个 c++问 ub 有什么区别?
    RogerBen
        22
    RogerBen  
       335 天前
    @xiaxiaocao
    我靠,为啥是 0 呢
    ememem0
        23
    ememem0  
       335 天前 via iPhone   ❤️ 2
    自己掌握的知识都是错的,还面试别人。面试者真的惨。
    geelaw
        24
    geelaw  
       335 天前   ❤️ 1
    @GopherDaily #14 错误的方式可以有很多,没有必然理由认为你的错误答案程度更轻,建议不要和别人玩“揣摩出题人意思”的游戏。

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

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

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

    换言之,A 对论证 B 是无意义的,论证 B 的惟一方式是阅读 Go 的定义。
    yxd19
        25
    yxd19  
       335 天前
    @geelaw 我不会写 go 啊。但是按你的说法如果我已经把一个变量传进一个闭包了,为啥复用这个变量的内存位置没问题啊?
    RedisMasterNode
        26
    RedisMasterNode  
       335 天前
    第一题不是 3 3 3 吗
    monkeyWie
        27
    monkeyWie  
       335 天前
    这种官方狗屎设计导致的 bug ,有什么好面的,新版本里都要修复了
    geelaw
        28
    geelaw  
       335 天前
    @yxd19 #25 你可能没有仔细阅读楼主的内容,请看第三段代码片段,那里面没有闭包。
    yxd19
        29
    yxd19  
       335 天前
    @geelaw 嗷嗷 确实 那没毛病
    rrfeng
        30
    rrfeng  
       335 天前
    @xiaxiaocao

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

    如果是 const x = 8 的话,const 会在编译的时候计算完 1<<x/2 的结果,于是能够放进 byte 里不会溢出。
    defunct9
        31
    defunct9  
       335 天前
    javascripts 应该是 3 3 3
    faker1
        32
    faker1  
       335 天前
    这个面试者太惨了,
    ensonmj
        33
    ensonmj  
       334 天前 via iPhone
    go 八股文
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1072 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 20ms · UTC 20:08 · PVG 04:08 · LAX 12:08 · JFK 15:08
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.