数据竞争与竞态检测器
本教程共 80 篇 · 第 61 篇 · 更新于 2026-07-27 · 约 8 分钟阅读
61. 数据竞争与竞态检测器
本节目标:理解什么是数据竞争,学会用
-race检测器发现竞争,认识几种最常见的并发错误。
什么是数据竞争
数据竞争(Data Race)指两个或多个 goroutine 同时访问同一个变量,且至少有一个在写。结果不可预测,是并发程序最难调试的 bug 之一。
看个经典例子:
var count int
func main() {
var wg sync.WaitGroup
wg.Add(1000)
for i := 0; i < 1000; i++ {
go func() {
defer wg.Done()
count++ // 读-改-写,不是原子操作
}()
}
wg.Wait()
fmt.Println(count) // 期望 1000,实际可能 987、993...
}
count++ 看起来是一行,实际是三步:读 count、加 1、写回。多个 goroutine 同时做这事,就会互相覆盖。
为什么结果不可预测
假设 count 当前是 5:
- goroutine A 读到 5
- goroutine B 也读到 5(在 A 写回之前)
- A 算出 6,写回
- B 算出 6,也写回
结果丢了 1。这种时序问题每次运行表现不同,调试时可能正常,上线后才爆雷。
-race 检测器
Go 内置了竞态检测器,加 -race 标志即可启用:
go run -race main.go # 运行时检测
go build -race -o app # 编译带检测的二进制
go test -race ./... # 测试时检测(最常用)
把上面的例子用 go run -race 跑,会输出类似:
==================
WARNING: DATA RACE
Read at 0x... by goroutine 7:
main.main.func1()
main.go:12 +0x3a
Previous write at 0x... by goroutine 6:
main.main.func1()
main.go:12 +0x44
==================
它会告诉你哪个变量、哪一行、哪些 goroutine 在冲突。
Tip
-race有开销,内存占用增加 5-10 倍,执行慢 2-20 倍。只在测试和开发环境用,别在生产环境长期开。可以在 CI 里跑go test -race作为常规检查。
常见错误一:共享变量
多个 goroutine 读写同一个普通变量,没有同步保护:
// 错误写法
var data int
go func() { data = 1 }()
go func() { fmt.Println(data) }() // 和上面的写竞争
解决办法:用 channel 或 sync.Mutex 保护:
var (
data int
mu sync.Mutex
)
go func() {
mu.Lock()
data = 1
mu.Unlock()
}()
go func() {
mu.Lock()
fmt.Println(data)
mu.Unlock()
}()
常见错误二:map 并发读写
Go 的内置 map 不是并发安全的,并发读写会直接 fatal:
m := make(map[int]int)
for i := 0; i < 10; i++ {
go func() {
m[1] = 1 // fatal error: concurrent map writes
}()
}
解决办法:用 sync.Map(后面讲)或加锁:
var m = make(map[int]int)
var mu sync.Mutex
go func() {
mu.Lock()
m[1] = 1
mu.Unlock()
}()
常见错误三:循环变量共享
Go 1.22 之前,for 循环变量是复用的,闭包捕获的是同一个变量:
// Go 1.21 及以前:有数据竞争
for i := 0; i < 5; i++ {
go func() {
fmt.Println(i) // 都可能打印 5
}()
}
Go 1.22 起修复了这个问题,每次迭代变量都是新的。但如果你用的是旧版本,或者变量声明在循环外面,仍然要注意。
老写法的修复:传参数拷贝一份:
for i := 0; i < 5; i++ {
go func(n int) {
fmt.Println(n) // 各自独立
}(i)
}
常见错误四:发送和关闭未同步
对同一个 channel,发送和关闭如果没同步好,也会竞争:
ch := make(chan struct{})
go func() { ch <- struct{}{} }()
close(ch) // 和上面的 send 竞争,可能 panic
解决办法:先确保发送完成再关闭,或者只让发送方关:
ch := make(chan struct{})
go func() {
ch <- struct{}{}
}()
<-ch // 等发送完成
close(ch)
检测器的局限
-race 只能发现运行时实际发生的竞争。如果你的测试没跑到那条代码路径,竞争就检测不到。所以:
- 测试覆盖率要高
- 可以在真实负载下跑带
-race的二进制 - 多跑几次,并发问题有随机性
Note竞态检测器需要 cgo 启用,在非 macOS 系统还需要 C 编译器。Windows 上需要 mingw-w64 runtime 8 或更高版本。
小结
- 数据竞争:多 goroutine 同时读写同一变量,至少一个在写
go test -race/go run -race启用检测器- 检测器报告冲突的变量、代码行和 goroutine
- 常见坑:共享变量、map 并发读写、循环变量、发送关闭未同步
-race有性能开销,开发测试用,别长期生产开- 检测器只能发现跑到的路径,测试覆盖要够
下一部分开始讲 sync 包,用锁和原子操作来保护共享数据。