参考文章LoopvarExperiment
从一道经典的js面试题说起
1
2
3
4
5
6
7
8
9
|
for (var i= 0 ; i <3 ;i++){
setTimeout(()=>{
console.log(i)
})
}
// output:
// 3
// 3
// 3
|
由于var
的作用域与setTimeout
的异步执行顺序问题. 将闭包函数同步推入异步队列, 异步执回调行时取i值为同步循环后的最终值, 即跳出循环的值3
golang (在go version 1.21.1下测试)中也有类似的问题
for
的声明语句中定义的变量会有值状态丢失的情况, 包括双分号和range
语法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
func main() {
done := make(chan bool)
for i := 0; i < 3; i++ {
go func() {
fmt.Println(i)
done <- true
}()
}
// chan blocks the code to reach the end before all the routines are done
for i := 0; i < 3; i++ {
<-done
}
}
// output:
// 3
// 3
// 3
|
同时借此可以类比拓展出其他类似js同异步打印问题
1
2
3
4
5
6
7
8
9
10
11
|
///...
go func() {
fmt.Println("second")
done <- true
}()
fmt.Println("first")
///...
// output:
// first
// second
|
推测也是go routine同异步导致的问题, go
关键字将回调开启并发, 也可以看作成一种异步.
但是 golang 中 go routine 不是异步队列模型, 所以不会保证输出顺序
解决方式也差不多, 本质上指定一个循环体局部变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
for i := 0; i < 3; i++ {
val := i
go func() {
fmt.Println(val)
done <- true
}()
/* alternative
go func(i int) {
fmt.Println(i)
done <- true
}(i)
*/
}
// output:
// 2
// 1
// 0 // not strictly ordered. can be 2 0 1 ....
|
这个问题在官方wiki上被称作为loopvar
, 1.21会有一个同名实验特性用来避免这个问题, 并且这个特性拟在1.22 默认开启
1
2
|
#add env variable when executing go commands
GOEXPERIMENT=loopvar go run main.go
|
输出结果:
