英文:
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 { ... }
中的k
和v
),但我们仍然必须首先将它们复制到一些本地闭包中,以使它们按预期工作。
这背后的原因是什么?(我怀疑这是因为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,1
,2,2
,...(当然,Go中的映射不一定是有序的,所以我们可能会看到3,3
十次作为最后一对值,而不是10,10
十次作为最后一对值)。在lab2
中的代码注释// (B)
处也是如此,这是预期的,因为我们试图在内部作用域中捕获外部变量(我也把这个放进去试试)。在lab3
中的代码注释// (C)
处一切正常,你将在那里看到十对数字,如1,1
,2,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(" ")
lab2() // captured closure is not what is expected
fmt.Println(" ")
lab3() // captured closure behaves ok
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) 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 <= 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 <= 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
参考资料:
函数字面量是闭包:它们可以引用周围函数中定义的变量。这些变量在周围函数和函数字面量之间共享,并且只要它们可访问,它们就会一直存在。
要将当前值v绑定到每个闭包中,需要修改内部循环以在每次迭代中创建一个新变量。一种方法是将变量作为参数传递给闭包。
更简单的方法是只需创建一个新变量,使用一种可能看起来奇怪但在Go中运行良好的声明样式即可。
英文:
Do you want the closure over the variable or the value? For example,
package main
import "fmt"
func VariableLoop() {
f := make([]func(), 3)
for i := 0; i < 3; i++ {
// closure over variable 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
// closure over value of 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 {
// closure over variable 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
// closure over value of i
f[i] = func() {
fmt.Println(i)
}
}
fmt.Println("ValueRange")
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 > var prints []func() > for i := 0; i < 3; i++ { > prints = append(prints, func() { println(i) }) > } > for _, p := range prints { > p() > } > > // 输出: > // 0 > // 1 > // 2 >
自从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
> var prints []func()
> for i := 0; i < 3; i++ {
> prints = append(prints, func() { println(i) })
> }
> for _, p := range prints {
> p()
> }
>
> // Output:
> // 0
> // 1
> // 2
>
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
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论