首页 / Go 语言入门教程 / 数据竞争与竞态检测器

Go 语言入门教程

数据竞争与竞态检测器

本教程共 80 篇 · 第 61 篇 · 更新于 2026-07-27 · 约 8 分钟阅读

GoGo 入门教程数据竞争race detector并发竞态条件go test -race

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:

  1. goroutine A 读到 5
  2. goroutine B 也读到 5(在 A 写回之前)
  3. A 算出 6,写回
  4. 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 只能发现运行时实际发生的竞争。如果你的测试没跑到那条代码路径,竞争就检测不到。所以:

  1. 测试覆盖率要高
  2. 可以在真实负载下跑带 -race 的二进制
  3. 多跑几次,并发问题有随机性
Note

竞态检测器需要 cgo 启用,在非 macOS 系统还需要 C 编译器。Windows 上需要 mingw-w64 runtime 8 或更高版本。

小结

  • 数据竞争:多 goroutine 同时读写同一变量,至少一个在写
  • go test -race / go run -race 启用检测器
  • 检测器报告冲突的变量、代码行和 goroutine
  • 常见坑:共享变量、map 并发读写、循环变量、发送关闭未同步
  • -race 有性能开销,开发测试用,别长期生产开
  • 检测器只能发现跑到的路径,测试覆盖要够

下一部分开始讲 sync 包,用锁和原子操作来保护共享数据。