在Golang项目里,并发编程虽然简单,但多个goroutine同时读写同一个变量会带来数据竞争。如果不用合适的方法检测,问题只会在高并发时偶发出现,很难排查。下面介绍几种实用的检测和写法。

什么是竞态条件
竞态条件指程序的最终结果依赖goroutine执行的顺序。当两个goroutine没有同步地修改同一块内存,就可能丢更新或读到脏数据。比如下面的计数器代码就不安全:
package main
import (
"fmt"
"time"
)
func main() {
count := 0
for i := 0; i < 1000; i++ {
go func() {
count++ // 并发自增,未加保护
}()
}
time.Sleep(time.Second)
fmt.Println(count) // 通常小于1000
}
使用Go自带的竞态检测器
Go在构建和测试时提供了race检测器,能动态发现数据竞争。只需在命令后加-race参数:
go test -race ./... go run -race main.go
如果代码存在竞争,终端会打印出具体的 goroutine 栈和冲突访问位置,方便定位。
用锁保护共享数据
最直观的办法是用sync.Mutex把临界区锁住:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var mu sync.Mutex
count := 0
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
count++
mu.Unlock()
}()
}
wg.Wait()
fmt.Println(count) // 稳定输出1000
}
用channel避免共享内存
Golang推荐通过通信来共享内存。把计数交给一个单独的goroutine处理,其他协程发消息即可:
package main
import "fmt"
func main() {
ch := make(chan int, 1000)
done := make(chan int)
// 计数协程
go func() {
sum := 0
for n := range ch {
sum += n
}
done <- sum
}()
for i := 0; i < 1000; i++ {
ch <- 1
}
close(ch)
fmt.Println(<-done) // 输出1000
}
编写并发单元测试
在测试文件中,可以用testing包反复跑并发逻辑,并配合-race使用:
package main
import (
"sync"
"testing"
)
func TestCounterSafe(t *testing.T) {
var mu sync.Mutex
count := 0
var wg sync.WaitGroup
for i := 0; i < 500; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
count++
mu.Unlock()
}()
}
wg.Wait()
if count != 500 {
t.Fatalf("期望500,实际%d", count)
}
}
小结
检测Golang并发数据安全,首选go test -race。日常编码尽量用channel或减少共享变量,必须共享时用Mutex或atomic包。写好并发测试,才能在早期暴露竞态问题。