在Go语言中,闭包(Closure)可以捕获循环变量。

huangapple go评论80阅读模式
英文:

Captured Closure (for Loop Variable) in Go

问题

Go编译器不应该将for...range循环变量捕获为本地分配的闭包变量吗?

长篇版本:

这在C#中也给我带来了一些困惑,我一直在试图理解它;为什么在C# 5.0中修复了foreach(原因:循环变量不能在循环体内部更改),但没有修复C#的for循环(原因:循环变量可以在循环体内部更改)。

现在(对我来说),Go中的for...range循环看起来很像C#中的foreach循环,但尽管我们不能更改这些变量(如for k, v := range m { ... }中的kv),但我们仍然必须首先将它们复制到一些本地闭包中,以使它们按预期工作。

这背后的原因是什么?(我怀疑这是因为Go以相同的方式处理任何for循环,但我不确定)。

这里有一些代码来检查所描述的行为:

func main() {
    lab1() // 捕获的闭包不是预期的
    fmt.Println(" ")

    lab2() // 捕获的闭包不是预期的
    fmt.Println(" ")

    lab3() // 捕获的闭包表现正常
    fmt.Println(" ")
}

func lab3() {
    m := make(map[int32]int32)
    var i int32
    for i = 1; i <= 10; i++ {
        m[i] = i
    }

    l := [](func() (int32, int32)){}
    for k, v := range m {
        kLocal, vLocal := k, v // (C) 仅捕获分配给k和v的正确值
        l = append(l, func() (int32, int32) {
            return kLocal, vLocal
        })
    }

    for _, x := range l {
        k, v := x()
        fmt.Println(k, v)
    }
}

func lab2() {
    m := make(map[int32]int32)
    var i int32
    for i = 1; i <= 10; i++ {
        m[i] = i
    }

    l := [](func() (int32, int32)){}
    for k, v := range m {
        l = append(l, func() (int32, int32) {
            kLocal, vLocal := k, v // (B) 仅捕获从范围中分配给k和v的最后一个值
            return kLocal, vLocal
        })
    }

    for _, x := range l {
        k, v := x()
        fmt.Println(k, v)
    }
}

func lab1() {
    m := make(map[int32]int32)
    var i int32
    for i = 1; i <= 10; i++ {
        m[i] = i
    }

    l := [](func() (int32, int32)){}
    for k, v := range m {
        l = append(l, func() (int32, int32) { return k, v }) // (A) 仅捕获从范围中分配给k和v的最后一个值
    }

    for _, x := range l {
        k, v := x()
        fmt.Println(k, v)
    }
}

lab1中所示,在注释// (A)处,我们只得到了来自范围的最后一个值;输出是像十次打印9,9而不是显示预期结果像1,12,2,...(当然,Go中的映射不一定是有序的,所以我们可能会看到3,3十次作为最后一对值,而不是10,10十次作为最后一对值)。在lab2中的代码注释// (B)处也是如此,这是预期的,因为我们试图在内部作用域中捕获外部变量(我也把这个放进去试试)。在lab3中的代码注释// (C)处一切正常,你将在那里看到十对数字,如1,12,2....

我试图将闭包+函数用作Go中元组的替代品。

英文:

Shouldn't Go compiler capture for...range loop variables as a locally assigned closure variable?

Long Version:

This caused me some confusion in C# too and I was trying to understand it; that why it is fixed in C# 5.0 foreach (reason: the loop variable can not change inside the body of loop) and the reasoning for not fixing it in C# for loops (reason: the loop variable can change inside the body of loop).

Now (to me) for...range loops in Go seems pretty much like foreach loops in C#, but despite the fact that we can not alter those variables (like k and v in for k, v := range m { ... }); still we have to copy them to some local closures first, for them to behave as expected.

What is the reasoning behind this? (I suspect it's because Go treats any for loop the same way; but I'm not sure).

Here is some code to examine described behavior:

func main() {
lab1() // captured closure is not what is expected
fmt.Println(&quot; &quot;)
lab2() // captured closure is not what is expected
fmt.Println(&quot; &quot;)
lab3() // captured closure behaves ok
fmt.Println(&quot; &quot;)
}
func lab3() {
m := make(map[int32]int32)
var i int32
for i = 1; i &lt;= 10; i++ {
m[i] = i
}
l := [](func() (int32, int32)){}
for k, v := range m {
kLocal, vLocal := k, v // (C) captures just the right values assigned to k and v
l = append(l, func() (int32, int32) {
return kLocal, vLocal
})
}
for _, x := range l {
k, v := x()
fmt.Println(k, v)
}
}
func lab2() {
m := make(map[int32]int32)
var i int32
for i = 1; i &lt;= 10; i++ {
m[i] = i
}
l := [](func() (int32, int32)){}
for k, v := range m {
l = append(l, func() (int32, int32) {
kLocal, vLocal := k, v // (B) captures just the last values assigned to k and v from the range
return kLocal, vLocal
})
}
for _, x := range l {
k, v := x()
fmt.Println(k, v)
}
}
func lab1() {
m := make(map[int32]int32)
var i int32
for i = 1; i &lt;= 10; i++ {
m[i] = i
}
l := [](func() (int32, int32)){}
for k, v := range m {
l = append(l, func() (int32, int32) { return k, v }) // (A) captures just the last values assigned to k and v from the range
}
for _, x := range l {
k, v := x()
fmt.Println(k, v)
}
}

As it is shown in lab1, at the comment // (A) we get just the last values from the range; the output is like printing 9,9 ten times instead of showing expected result like 1,1, 2,2, ... (and of-course maps are not necessarily sorted in Go so we may see 3,3 ten times as the last pair of values; instead of 10,10 ten times as the last pair of values). The same goes for code at comment // (B) at lab2, which was expected because we are trying to capture outer variables inside the inner scope (I put this one too just to try that). In lab3 at code at comment // (C) everything works fine and you will see ten pairs of numbers there like 1,1, 2,2, ....

I was trying to use closure+function as a replacement for tuples in Go.

答案1

得分: 19

你想要对变量还是值进行闭包?例如,

package main
import "fmt"
func VariableLoop() {
f := make([]func(), 3)
for i := 0; i < 3; i++ {
// 对变量 i 进行闭包
f[i] = func() {
fmt.Println(i)
}
}
fmt.Println("VariableLoop")
for _, f := range f {
f()
}
}
func ValueLoop() {
f := make([]func(), 3)
for i := 0; i < 3; i++ {
i := i
// 对 i 的值进行闭包
f[i] = func() {
fmt.Println(i)
}
}
fmt.Println("ValueLoop")
for _, f := range f {
f()
}
}
func VariableRange() {
f := make([]func(), 3)
for i := range f {
// 对变量 i 进行闭包
f[i] = func() {
fmt.Println(i)
}
}
fmt.Println("VariableRange")
for _, f := range f {
f()
}
}
func ValueRange() {
f := make([]func(), 3)
for i := range f {
i := i
// 对 i 的值进行闭包
f[i] = func() {
fmt.Println(i)
}
}
fmt.Println("ValueRange")
for _, f := range f {
f()
}
}
func main() {
VariableLoop()
ValueLoop()
VariableRange()
ValueRange()
}

输出结果:

VariableLoop
3
3
3
ValueLoop
0
1
2
VariableRange
2
2
2
ValueRange
0
1
2

参考资料:

Go编程语言规范

函数字面量

函数字面量是闭包:它们可以引用周围函数中定义的变量。这些变量在周围函数和函数字面量之间共享,并且只要它们可访问,它们就会一直存在。

Go常见问题:闭包作为goroutine运行时会发生什么?

要将当前值v绑定到每个闭包中,需要修改内部循环以在每次迭代中创建一个新变量。一种方法是将变量作为参数传递给闭包。

更简单的方法是只需创建一个新变量,使用一种可能看起来奇怪但在Go中运行良好的声明样式即可。

英文:

Do you want the closure over the variable or the value? For example,

package main
import &quot;fmt&quot;
func VariableLoop() {
f := make([]func(), 3)
for i := 0; i &lt; 3; i++ {
// closure over variable i
f[i] = func() {
fmt.Println(i)
}
}
fmt.Println(&quot;VariableLoop&quot;)
for _, f := range f {
f()
}
}
func ValueLoop() {
f := make([]func(), 3)
for i := 0; i &lt; 3; i++ {
i := i
// closure over value of i
f[i] = func() {
fmt.Println(i)
}
}
fmt.Println(&quot;ValueLoop&quot;)
for _, f := range f {
f()
}
}
func VariableRange() {
f := make([]func(), 3)
for i := range f {
// closure over variable i
f[i] = func() {
fmt.Println(i)
}
}
fmt.Println(&quot;VariableRange&quot;)
for _, f := range f {
f()
}
}
func ValueRange() {
f := make([]func(), 3)
for i := range f {
i := i
// closure over value of i
f[i] = func() {
fmt.Println(i)
}
}
fmt.Println(&quot;ValueRange&quot;)
for _, f := range f {
f()
}
}
func main() {
VariableLoop()
ValueLoop()
VariableRange()
ValueRange()
}

Output:

<pre>
VariableLoop
3
3
3
ValueLoop
0
1
2
VariableRange
2
2
2
ValueRange
0
1
2
</pre>

References:

> The Go Programming Language Specification
>
> Function literals
>
> Function literals are closures: they may refer to variables defined in
> a surrounding function. Those variables are then shared between the
> surrounding function and the function literal, and they survive as
> long as they are accessible.
>
> Go FAQ: What happens with closures running as goroutines?
>
> To bind the current value of v to each closure as it is launched, one
> must modify the inner loop to create a new variable each iteration.
> One way is to pass the variable as an argument to the closure.
>
> Even easier is just to create a new variable, using a declaration
> style that may seem odd but works fine in Go.

答案2

得分: 1

Common Mistake / Using reference to loop iterator variable wiki页面记录了你的过程(将循环变量复制到一个新变量中)。

但是,2023年5月的提案# 60078 spec: less error-prone loop variable scoping旨在解决这个问题。

> 改变循环语义实质上会为每个使用:=声明的循环变量插入这种v := v语句。它将修复这个循环和许多其他循环,以实现作者明确的意图。
>
> 新的循环语义只适用于选择了具有新循环的发布的Go模块。如果是Go 1.22,那么只有在go.mod中声明了go 1.22的模块中的包才会获得新的循环语义。

你可以在这里看到正在考虑的设计

> 这个提案是关于改变循环变量作用域语义,使循环变量成为每次迭代而不是每次循环。

带有for子句的For语句将包括:

> init语句可以是短变量声明(:=),但后置语句不能是。
每次迭代都有自己单独的声明变量(或变量)。
>
> - 第一次迭代使用init语句声明变量。
> - 每个后续迭代使用隐式声明的变量在执行后置语句之前初始化,并将其初始化为上一次迭代的变量的值。
>
> go &gt; var prints []func() &gt; for i := 0; i &lt; 3; i++ { &gt; prints = append(prints, func() { println(i) }) &gt; } &gt; for _, p := range prints { &gt; p() &gt; } &gt; &gt; // 输出: &gt; // 0 &gt; // 1 &gt; // 2 &gt;


自从Go 1.21发布以来,Russ Cox添加

> 对于任何对这个问题感到陌生并想要了解这个变化意味着什么的人,Go playground现在可以让你尝试新的语义。
>
> 要做到这一点,使用Go 1.21并在程序顶部添加// GOEXPERIMENT=loopvar
>
> 例如,尝试https://go.dev/play/p/lDFLrPOcdz3,然后尝试删除注释。

// GOEXPERIMENT=loopvar

package main

func main() {
	var prints []func()
	for i := range make([]int, 5) {
		prints = append(prints, func() { println(i) })
	}
	for _, p := range prints {
		p()
	}
}

输出(带有注释):

0
1
2
3
4

输出(没有注释):

4
4
4
4
4
英文:

The Common Mistake / Using reference to loop iterator variable wiki page document your process (copy the loop variable into a new variable)

But the proposal # 60078 spec: less error-prone loop variable scoping, May 2023, seek to address that.

> Changing the loop semantics would in essence insert this kind of v := v statement for every for loop variable declared with :=. It would fix this loop and many others to do what the author clearly intends.
>
> the new loop semantics would only apply in Go modules that have opted in to the release with the new loops. If that was Go 1.22, then only packages in a module with a go.mod that says go 1.22 would get the new loop semantics.

You can see the design being considered here:

> This proposal is about changing for loop variable scoping semantics, so that loop variables are per-iteration instead of per-loop.

The For statements with for clause would include:

> The init statement may be a short variable declaration (:=), but the post statement must not.
Each iteration has its own separate declared variable (or variables).
>
> - The variable used by the first iteration is declared by the init statement.
> - The variable used by each subsequent iteration is declared implicitly before executing the post statement and initialized to the value of the previous iteration's variable at that moment.
>
> go
&gt; var prints []func()
&gt; for i := 0; i &lt; 3; i++ {
&gt; prints = append(prints, func() { println(i) })
&gt; }
&gt; for _, p := range prints {
&gt; p()
&gt; }
&gt;
&gt; // Output:
&gt; // 0
&gt; // 1
&gt; // 2
&gt;


Since Go 1.21 is released, Russ Cox adds

> For anyone new to this issue and exploring what the change would mean, the Go playground now lets you experiment with the new semantics.
>
> To do that, use Go 1.21 and add // GOEXPERIMENT=loopvar at the top of your program.
>
> For example try https://go.dev/play/p/lDFLrPOcdz3 and then try deleting the comment.

// GOEXPERIMENT=loopvar

package main

func main() {
	var prints []func()
	for i := range make([]int, 5) {
		prints = append(prints, func() { println(i) })
	}
	for _, p := range prints {
		p()
	}
}

Output (with comment):

0
1
2
3
4

Output (without comment):

4
4
4
4
4

huangapple
  • 本文由 发表于 2014年11月2日 04:20:46
  • 转载请务必保留本文链接:https://go.coder-hub.com/26692844.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定