GO闭包【4】“普通闭包”与“循环闭包”之间捕获的核心区别

循环闭包

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")中:

  1. 主协程 拿到了地址 A 的钥匙。
  2. 子协程 也拿到了地址 A 的钥匙。
  3. 子协程动作快,在地址 A 写入了 "f"。
  4. 主协程去地址 A 读取,读到了 "f"。

这就是报错的原因:虽然这一轮的 v 确实是"专属"的,但它是被主协程和子协程共享的。两个协程在没有任何保护的情况下,同时对"地址 A"一读一写,Go 的探测器(-race)立刻就报警了。

结论(一句话总结)

Go 1.22 只是帮你解决了 "下一轮循环会把这一轮变量改掉" 的问题(即:跨轮安全);

但它并没有、也不可能帮你解决 "你亲手开的子协程和主协程打架" 的问题(即:同轮安全)。

所以,想要绝对安全,终极方案依然是:

要么传参(给子协程发个"照片",随便它怎么涂鸦都不影响原件),要么加锁。

你这个例子其实非常牛,它证明了"变量独立"不等于"并发安全"。要不要试试把 v = "f" 删掉再运行 -race,看看警告是不是瞬间消失了?

相关推荐
FungLeo16 小时前
成为全栈·Go 后端篇·跨语言认证兼容:Node 与 Go 如何互认 JWT 和密码
golang·go·jwt·身份认证·成为全栈·bcrypt
FungLeo20 小时前
成为全栈·Go 后端篇·重写之前先锁基线:契约、Node 版本与行为清单
golang·node.js·go·openapi·契约测试·成为全栈
小满zs1 天前
Go语言第十七章(反射)
后端·go
小满zs1 天前
Go语言第十五章(JSON序列化)
后端·go
小满zs1 天前
Go语言第十六章(自定义类型)
后端·go
w-w0w-w1 天前
Go语言学习day1
go
FungLeo2 天前
成为全栈·Go 后端篇·契约一致性与差分测试:两套实现如何互相校验
golang·node.js·go·合约测试·差分测试·成为全栈·api兼容
vipxieliang2 天前
Go 1.25 Green Tea GC 深度解析:以页为单位的垃圾回收新范式
go
孔令飞2 天前
AI 时代岗位调研分析:调研了近3000份AI岗位描述,分享下调研结果
云原生·go·ai编程
我不会起名字3222 天前
Redis 从 0 到 1(十二):先删缓存还是先更新库?4 种方案的 Go 并发实测
redis·go·并发·延迟双删·缓存一致性