什么是数据竞争
数据竞争(Data Race)指多个 goroutine 未同步地访问同一共享变量,且至少有一个是写操作。它带来三个后果:行为不可预测(读到旧值/中间值)、可能内存损坏或崩溃、且只在特定时序下复现——是最难调试的并发 bug 之一。
Go 内置竞态检测器(-race)
Go 从 1.1 起内置竞态检测器,编译/运行/测试时加 -race 即可:
go build -race main.go
go run -race main.go
go test -race ./...
原理:编译器在读写内存处插入检测代码,运行时发现”未同步的冲突访问”就打印详细报告(包括冲突的两个 goroutine 堆栈和代码位置):
WARNING: DATA RACE
Write at 0x00c00009c008 by goroutine 6:
main.main.func1()
Read at 0x00c00009c008 by main goroutine:
main.main()
常见竞争场景与修复
1. 未同步的共享变量
var counter int
go func() { counter++ }() // 写
fmt.Println(counter) // 读,竞争!
修复:互斥锁或原子操作。
var mu sync.Mutex
mu.Lock(); counter++; mu.Unlock()
// 或
atomic.AddInt64(&counter, 1)
2. 循环闭包捕获循环变量
for i := 0; i < 5; i++ {
go func() { fmt.Println(i) }() // Go 1.21 及以前:i 共享,输出不确定
}
// 修复:传参(Go 1.22+ 循环变量已每次迭代独立,此坑已消除)
go func(i int) { fmt.Println(i) }(i)
3. channel 同步的使用注意
var data int
done := make(chan bool)
go func() { data = 1; done <- true }()
<-done
fmt.Println(data) // channel 同步保证可见性,OK
原则:channel 的”发送”与”接收”之间构成 happens-before,用它传递数据是安全的;但同一份数据既走 channel 又裸读裸写,就会竞争。
检测器的局限性
- 性能开销:内存占用增加 5–10 倍、执行时间延长 2–20 倍——只用于开发和测试,不带入生产;
- 平台限制:仅支持 64 位平台(linux/amd64、windows/amd64 等);
- 非万能:只检测”实际发生”的竞争,测不到就不报;也不管死锁、逻辑错误。
最佳实践
- CI 里默认跑
go test -race ./...,把竞争挡在上线前; - 优先用 channel 传数据而不是共享变量;
- 必须共享时用互斥锁/原子操作,并保持锁粒度尽量小;
- 配合
go vet(静态)+-race(动态)双管齐下。



