循环闭包
Go
package main
import (
"fmt"
"time"
)
func main() {
values := []string{"a", "b", "c"}
for _, v := range values {
time.Sleep(1 * time.Second)
go func() {
fmt.Println(time.Now().Format("04:05"), v)
v = "f"
fmt.Println(time.Now().Format("04:05"), v)
}()
time.Sleep(1 * time.Second)
fmt.Println(time.Now().Format("04:05"), v)
}
time.Sleep(8 * time.Second)
}
执行结果
32:01 a
32:01 f
==================
WARNING: DATA RACE
Read at 0x00c00009e030 by main goroutine:
main.main()
main.go:18 +0x128
Previous write at 0x00c00009e030 by goroutine 9:
main.main.func1()
D:/Codes/test/main.go:14 +0xee
Goroutine 9 (finished) created at:
main.main()
D:/Codes/test/main.go:12 +0xc5
==================
32:02 f
32:03 b
32:03 f
32:04 f
32:05 c
32:05 f
32:06 f
Found 1 data race(s)
exit status 66
普通闭包
Go
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
fmt.Println("--- 开始执行 ---")
help := "help you"
wg.Add(3)
go func() {
defer wg.Done()
fmt.Println("1:", help)
}()
go func() {
help = "help you too"
fmt.Println("2:", help)
wg.Done()
}()
go func() {
defer wg.Done()
time.Sleep(1 * time.Second)
fmt.Println("3:", help)
}()
fmt.Println("before wait:", help)
wg.Wait()
fmt.Println("after wait:", help)
fmt.Println("--- 执行结束 ---")
}
执行结果
--- 开始执行 ---
before wait: help you
1: help you
2: help you too
3: help you too
after wait: help you too
--- 执行结束 ---
Go 1.22 新特性时最容易产生困惑的地方。
"每一轮的 v 都是独立的",指的是轮与轮之间(Iteration-to-Iteration)互不干扰。但现在的代码展示了另一个维度:同一轮内部(Within one Iteration)。
我们来拆解一下:
1. "独立"不代表"私有"
- 轮与轮独立:第一轮循环的变量
v(地址 A)和第二轮循环的变量v(地址 B)是两个不同的变量。所以第二轮的b不会覆盖第一轮的a。 - 同一轮共享:在第一轮循环里,你的
main协程 和 子协程 面对的是同一个变量v(地址 A)。
2. 例子里发生了什么?
在第一轮循环(v = "a")中:
- 主协程 拿到了地址 A 的钥匙。
- 子协程 也拿到了地址 A 的钥匙。
- 子协程动作快,在地址 A 写入了
"f"。 - 主协程去地址 A 读取,读到了
"f"。
这就是报错的原因:虽然这一轮的 v 确实是"专属"的,但它是被主协程和子协程共享的。两个协程在没有任何保护的情况下,同时对"地址 A"一读一写,Go 的探测器(-race)立刻就报警了。
结论(一句话总结)
Go 1.22 只是帮你解决了 "下一轮循环会把这一轮变量改掉" 的问题(即:跨轮安全);
但它并没有、也不可能帮你解决 "你亲手开的子协程和主协程打架" 的问题(即:同轮安全)。
所以,想要绝对安全,终极方案依然是:
要么传参(给子协程发个"照片",随便它怎么涂鸦都不影响原件),要么加锁。
你这个例子其实非常牛,它证明了"变量独立"不等于"并发安全"。要不要试试把 v = "f" 删掉再运行 -race,看看警告是不是瞬间消失了?