Go 竞态检测

什么是数据竞争

数据竞争(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(动态)双管齐下。
滚动至顶部