Golang 1.22版本中for-loop变量语义的变更

Golang 1.22版本中for-loop变量语义的变更 在 Go 1.22 版本中列出的第一个更改是为了修复一个曾多次困扰我(可能还有无数其他人)的意外行为,该更改修改了 for 循环的语义:

此前,由“for”循环声明的变量只创建一次,并在每次迭代中更新。在 Go 1.22 中,循环的每次迭代都会创建新的变量,以避免意外的共享错误。

下面是一个简单的演示,它在 Go 1.21 和 Go 1.22 中会打印出不同的结果:

package main

import (
    "fmt"
)

func main() {
    var funcs []func()
    for i := 0; i < 2; i++ {
        funcs = append(funcs, func() {fmt.Printf("%d\n", i)})
    }
    for _, f := range funcs {
        f()
    }
}

在 Go 1.22 中,程序的行为符合大多数程序员的预期,打印出 0, 1。闭包使用的是闭包创建时 i 的值。这个变量在循环的每次迭代中都是不同的。

在 Go 1.21 及更早版本中,它打印出 2, 2。 变量 i 只被创建了一次,而使用它的闭包是通过引用来使用它的。由于闭包是在循环执行完毕后执行的,此时 i 的值为 “2”。

我在论坛上搜索了关于旧行为的讨论,找到了这个:

这里有一个片段:https://play.golang.org/p/cKrC0oy7FP

package main

import (
    "fmt"
    "time"
)

type field struct {
    name string
}

func (p *field) print() {
    fmt.Println(p.name)
}

func TestClosure() {

    data := []*field{{"one"}, {"two"}, {"three"}}

    for _, v := range data {
        go v.print()
    }

    time.Sleep(3 * time.Second)
}

func TestClosure1() {

    data := []field{{"one"}, {"two"}, {"three"}}

    for _, v := range data {
        go v.print()
    }

    time.Sleep(…

遗憾的是,我无法在那里添加评论来指出行为的改变。


更多关于Golang 1.22版本中for-loop变量语义的变更的实战教程也可以访问 https://www.itying.com/category-94-b0.html

2 回复

是的——这是一项“破坏性”变更,其副作用可能仅仅是修复一些细微的错误。

更多关于Golang 1.22版本中for-loop变量语义的变更的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


Go 1.22 中 for 循环变量语义的变更是 Go 语言的一个重要改进,解决了长期存在的闭包捕获循环变量时的意外行为问题。以下是详细说明和示例:

在 Go 1.21 及更早版本中,for 循环声明的变量(如 iv)在循环的整个生命周期中只创建一次,每次迭代更新其值。这会导致闭包捕获的是变量的引用,而不是迭代时的值。例如:

package main

import "fmt"

func main() {
    var funcs []func()
    for i := 0; i < 2; i++ {
        funcs = append(funcs, func() { fmt.Println(i) })
    }
    for _, f := range funcs {
        f()
    }
}

在 Go 1.21 中,输出为:

2
2

在 Go 1.22 中,每次迭代都会创建新的变量实例,闭包捕获的是迭代时的值。因此,相同的代码在 Go 1.22 中输出:

0
1

另一个常见场景是在 goroutine 中使用循环变量。在 Go 1.21 中:

package main

import (
    "fmt"
    "time"
)

func main() {
    data := []string{"one", "two", "three"}
    for _, v := range data {
        go func() {
            fmt.Println(v)
        }()
    }
    time.Sleep(time.Second)
}

输出可能为:

three
three
three

在 Go 1.22 中,每次迭代的 v 是新变量,输出为:

one
two
three

对于结构体切片,类似的问题也会被修复。在 Go 1.21 中:

package main

import (
    "fmt"
    "time"
)

type field struct {
    name string
}

func (p *field) print() {
    fmt.Println(p.name)
}

func main() {
    data := []field{{"one"}, {"two"}, {"three"}}
    for _, v := range data {
        go v.print()
    }
    time.Sleep(time.Second)
}

在 Go 1.21 中,可能输出重复值。在 Go 1.22 中,每次迭代的 v 是新变量,输出正确。

此变更仅影响在循环体中声明的变量,不影响循环后使用的变量。例如:

package main

import "fmt"

func main() {
    var i int
    for i = 0; i < 2; i++ {
        // i 是外部变量,行为不变
    }
    fmt.Println(i) // 输出 2
}

对于需要向后兼容的代码,Go 1.22 提供了 GOEXPERIMENT=loopvar 环境变量来控制行为,但建议直接适配新语义。

回到顶部