必须用 go test -race,否则无法可靠暴露数据竞争;它能精确定位读写冲突,需通过混合读写、调度压力等手段触发竞态,修复后需 -race 零警告且配合 goroutine 泄漏检测才算安全。

必须用 go test -race,否则等于没测
Go 的竞态检测器不是辅助工具,是唯一能可靠暴露数据竞争的机制。不加 -race 运行的并发测试,99% 的问题根本不会报错——你看到的“测试通过”,只是运气好,不是安全。
- 本地跑
go test全绿?CI 或线上偶发 panic / 返回脏数据 / 计数器少几十?大概率就是漏了-race -
go run -race main.go也能临时验证,但只用于调试;生产构建绝不能带-race,它会让程序慢 5–10 倍、内存翻倍 - 报错信息非常具体,比如:
Read at 0x00c000012340 by goroutine 7+ 调用栈,直接定位到哪一行读、哪一行写、谁在干这事 - 对只读全局变量(如初始化后不再改的
var config = struct{}{})不会误报——这是合法的,不用加锁
怎么写才能让 -race 真的报警?
不是起几个 goroutine 就算并发测试。要让竞态“大概率触发”,得主动制造调度压力和临界区碰撞机会。
- 用
sync.WaitGroup控制并发数量,比如 10 个 goroutine 各调 100 次Inc(),最后检查总数是否为 1000 - 避免
time.Sleep—— 它掩盖问题,还拖慢测试;改用runtime.Gosched()在临界区前后插桩,强制调度切换 - 混合读写操作:比如缓存测试里,80% goroutine 调
Get(),20% 调Set(),比纯写更容易暴露读写冲突 - 别复用测试对象:每个子测试用
t.Run()隔离,重新初始化被测结构体(如新造一个cache),防止状态污染
修复后怎么确认真安全了?
加了 sync.Mutex 或 atomic.AddInt64 不代表就完了。-race 静默才是终点。
- 优先用
atomic处理简单数值(计数器、flag),性能好、无锁;涉及多字段或条件逻辑(比如“如果未过期才返回”),必须用sync.Mutex或sync.RWMutex - 锁必须覆盖所有访问路径:写了
mu.Lock()在Set()里,但忘了在Get()里加mu.RLock()?-race会立刻打脸 - 修复后必须重跑
go test -race,零警告才算过关;别信“看起来没 panic” - 顺手加
go.uber.org/goleak:在测试前后调goleak.VerifyNone(t),揪出忘记close()channel 或没退出的后台 goroutine
别忽略 goroutine 泄漏和超时卡死
并发测试挂住不动、或跑完发现 goroutine 数量持续上涨,不是竞态,但一样致命。
- 用
runtime.NumGoroutine()在测试前后打点,差值异常说明有泄漏 - 别用
time.Sleep等待结果;用channel+select配合超时,比如select { case - 涉及 context 的操作(如带 cancel 的后台任务),测试里一定要显式调
cancel()并等待 goroutine 退出,否则goleak会报警 - 如果测试中用了
t.Parallel(),确保它不操作任何共享资源(比如全局 map、文件句柄),否则可能引入新竞态
并发安全不是“写完再加个锁”,而是从测试设计开始就逼自己暴露问题。最常被跳过的一步,就是没坚持每次修改都跑 go test -race —— 一次遗漏,就可能埋下线上静默故障的种子。










