★ 数据竞争检测器 race 怎么用?原理是什么?
一句话结论:race 是 Go 官方内置的数据竞争检测器,把 -race 传给 go build / go test / go run 即可开启;它通过编译期插桩加运行时 happens-before 追踪,在“两个 goroutine 无同步地并发访问同一内存且至少一个是写”时给出详细报告,是排查并发 bug 的第一工具。
使用方法
- 测试:go test -race ./...(最常用,建议在 CI 里常开)。
- 运行与构建:go run -race main.go、go build -race xxx。
- 只对“实际执行到的代码路径”有效:没跑到就不会报,所以测试要充分触发并发分支。
工作原理
- 编译器对每个内存读写指令插入检测代码,记录访问的 goroutine 与内存地址。
- 运行库为每个 goroutine 维护向量时钟(vector clock),用来近似 happens-before 关系。
- 当检测到两个访问同一地址、之间无 happens-before、且至少一个为写时,判定为数据竞争并输出报告。
- 报告包含两个冲突访问的完整 goroutine 栈、变量地址与创建点,帮你定位“谁在写、谁在读、该在哪做同步”。
开销与边界
| 维度 | 说明 |
|---|---|
| CPU | 通常慢 2~20 倍(视程序而定) |
| 内存 | 通常增加 5~10 倍 |
| 覆盖 | 只覆盖执行过的代码路径 |
| 不检测 | 死锁、逻辑错误、以及“没跑到”的竞争 |
race 没报告竞争,不代表程序绝对正确,只说明本次执行没观察到竞争。
最小复现
func TestRace(t *testing.T) {
var n int
go func() {
n++ // 写
}()
n++ // 读改写,与上面的写无同步
_ = n
}
执行 go test -race ./... 会输出 “WARNING: DATA RACE”,给出两个访问栈与 goroutine 创建位置;把一侧放进 sync.Mutex 或改用 atomic 后报告消失。
常见追问 / 记忆点
- 追问:加了锁还是报竞争?——通常是某条路径漏加锁,或加的不是同一把锁。
- 追问:race 能发现死锁吗?——不能,它是竞争检测器,不是死锁检测器。
- 记忆点:竞争三要素“共享 + 并发 + 无同步”;race 只抓执行路径,CI 里常开 -race。