IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    每个Go程序员必犯之错

    smallnest发表于 2023-12-17 10:37:03
    love 0

    说起每个程序员必犯的错误,那还得是"循环变量"这个错误了,就连Go的开发者都犯过这个错误,这个错误在Go的FAQ中也有提到What happens with closures running as goroutines?:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    func main() {
    var wg sync.WaitGroup
    values := []string{"a", "b", "c"}
    for _, v := range values {
    wg.Add(1)
    go func() {
    fmt.Println(v)
    wg.Done()
    }()
    }
    wg.Wait()
    }

    你可能期望能输出a、b、c这三个字符(可能顺序不同),但是实际可能输出的是c、c、c。
    这是因为循环变量的作用域是整个循环,而不是单次迭代,所以在循环体中使用的变量是同一个变量,而不是每次迭代都是一个新的变量。

    这个错误有时候隐藏很深,即使没有goroutine,也有可能,比如下面的代码,并没有使用额外的goroutine和闭包,也是有问题的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    package main
    import (
    "fmt"
    )
    type Char struct {
    Char *string
    }
    func main() {
    var chars []Char
    values := []string{"a", "b", "c"}
    for _, v := range values {
    chars = append(chars, Char{Char: &v})
    }
    for _, v := range chars {
    fmt.Println(*v.Char)
    }
    }

    输出也大概率是c、c、c,因为给每个Char的字段赋值的是v的指针,v在整个循环中都是一个变量,所以最后的结果都是c。

    Go团队很早也意识到这个问题了,但是考虑到兼容的问题,大家的容忍程度,那就这样了。每个Go程序员都在这里摔一跤,也就长记性了,所以一直没有改变这个设计。我在这里摔了好多跤,以至于我写for循环的时候都战战兢兢的,和Russ Cox统计的网上的处理一样,不管有无必要,很多时候我都是先把循环变量赋值给一个局部变量,然后再使用,比如下面的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    for _, v := range values {
    v := v
    wg.Add(1)
    go func() {
    fmt.Println(v)
    wg.Done()
    }()
    }

    今年5月份的时候,Russ Cox忍不住了,提了一个提案#60078,提案的内容是在for循环中,如果变量只在循环体中使用,那么就会在每次迭代中创建一个新的变量,而不是使用同一个变量。这个提案引起了很多人的关注,很多人都在讨论这个提案,这个提案被接收了,具体提案内容在文档中Proposal: Less Error-Prone Loop Variable Scoping。

    如果你使用Go 1.21, 你可以开始这个功能,使用GOEXPERIMENT=loopvar go run main.go运行上面的程序,会输出c、b、a这样的输出,不再是c、c、c了。
    这个特性在Go 1.22中会默认开启,不需要设置GOEXPERIMENT了。还有一两个月才能正式发布go 1.22,大家可以使用gotip测试:

    1
    2
    3
    4
    $ gotip run main.go
    a
    b
    c

    不只是for-range,下面的3-clause也是同样的问题:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    func main() {
    var ids []*int
    for i := 0; i < 3; i++ {
    i = 10
    }
    for _, id := range ids {
    fmt.Println(*id)
    }
    }

    Go 1.22中也会修复这个问题。C#语言就只修改了for-range语句,3-clause语句就没有修改, Go两种都做了修改。

    但是, 问题就来了哈,像下面的代码,Go 1.22和以前的代码会一样么?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    func main() {
    var ids []*int
    for i := 0; i < 3; i++ {
    i = 10
    ids = append(ids, &i)
    }
    for _, id := range ids {
    fmt.Println(*id)
    }
    }

    如果用Go 1.21,它会输出11。
    如果用Go 1.22,它会输出10。

    看起来打破了向下兼容的承诺,你如果先前就想利用这个corner case的话,Go1.22已经不兼容了。



沪ICP备19023445号-2号
友情链接