引入

多个 goroutine 同时读写同一个变量,结果可能每次运行都不一样——这就是竞态条件。并发安全是写 Go 程序绕不开的话题,Go 提供了 sync.Mutexsync.Onceatomic 包等工具来保证并发场景下数据的正确性。

正文

定义

定义

竞态条件(race condition)指多个 goroutine 同时访问共享数据,且最终结果依赖执行顺序。并发安全指程序在并发环境下依然能得到正确结果。Go 用 sync.Mutex 加锁、sync.Once 保证只执行一次、atomic 包做原子操作来保障并发安全。

语法

// sync.Mutex
var mu sync.Mutex
mu.Lock()
// 操作共享数据
mu.Unlock()
 
// sync.Once
var once sync.Once
once.Do(func() {
    // 只执行一次
})
 
// atomic 原子操作
var count int64
atomic.AddInt64(&count, 1)
atomic.LoadInt64(&count)
atomic.StoreInt64(&count, 42)

例子

竞态条件演示

func main() {
    count := 0
    var wg sync.WaitGroup
 
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            count++ // 不是原子操作:读→加→写
        }()
    }
 
    wg.Wait()
    fmt.Println(count) // 小于 1000(每次可能不同)
}

count++ 看似一行,实际是读取、加一、写回三步,多个 goroutine 交错执行会丢失更新。

sync.Mutex 修复竞态

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
}

加锁后同一时刻只有一个 goroutine 执行 count++,结果正确。

defer 解锁防止遗漏

func safeIncrement(mu *sync.Mutex, count *int) {
    mu.Lock()
    defer mu.Unlock() // 即使中间 panic 也能解锁
    *count++
}

defer mu.Unlock() 确保函数退出时一定解锁,避免死锁。

sync.Once 只执行一次

var once sync.Once
var config string
 
func loadConfig() {
    once.Do(func() {
        fmt.Println("加载配置...")
        config = "loaded"
    })
}
 
func main() {
    var wg sync.WaitGroup
 
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            loadConfig()
        }()
    }
 
    wg.Wait()
    fmt.Println(config) // loaded("加载配置..." 只打印一次)
}

sync.Once 保证 Do() 里的函数无论被调用多少次都只执行一次,适合初始化操作。

atomic 原子操作

var count int64
var wg sync.WaitGroup
 
for i := 0; i < 1000; i++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        atomic.AddInt64(&count, 1)
    }()
}
 
wg.Wait()
fmt.Println(count) // 1000

atomic.AddInt64()原子操作,不会被其他 goroutine 打断,比 Mutex 更轻量,适合简单的计数器场景。

atomic 常用操作

var val int64
 
atomic.StoreInt64(&val, 100)       // 写入
fmt.Println(atomic.LoadInt64(&val)) // 读取:100
 
atomic.AddInt64(&val, 10)          // 加
fmt.Println(atomic.LoadInt64(&val)) // 110
 
old := atomic.SwapInt64(&val, 0)   // 交换,返回旧值
fmt.Println(old)                    // 110
 
ok := atomic.CompareAndSwapInt64(&val, 0, 50) // CAS
fmt.Println(ok, val) // true 50
函数说明
atomic.AddInt64原子加法
atomic.LoadInt64原子读取
atomic.StoreInt64原子写入
atomic.SwapInt64原子交换
atomic.CompareAndSwapInt64原子 CAS(比较并交换)

用 race 检测器找竞态

go run -race main.go
go test -race ./...

Go 内置 race 检测器,编译时加 -race 就能在运行时检测数据竞争。开发阶段建议始终开启

常见写法

写法说明
mu.Lock() / defer mu.Unlock()加锁保护共享资源
once.Do(func() {…})保证只执行一次
atomic.AddInt64(&v, 1)原子自增
atomic.LoadInt64(&v)原子读取
atomic.StoreInt64(&v, n)原子写入
atomic.CompareAndSwapInt64CAS 操作
go run -race检测数据竞争

特点

  • 竞态条件源于多个 goroutine 同时读写共享数据,结果不可预测
  • sync.Mutex 是最通用的加锁方式,适合复杂操作
  • defer mu.Unlock()最佳实践,防止忘记解锁
  • sync.Once 保证函数只执行一次,适合懒加载、初始化
  • atomic 包提供原子操作,比 Mutex 更轻量,适合简单数值操作
  • atomic 只能操作 int32/int64/uint32/uint64/uintptr/Pointer 等类型
  • -race 检测器是发现竞态问题的利器,开发和测试时推荐使用

理解

并发安全的核心就是一句话:别让多个 goroutine 同时改同一个东西。简单的计数器用 atomic,复杂操作加 Mutex,初始化用 sync.Once。养成用 -race 检测的习惯,能在上线前把竞态 bug 揪出来。

引出

并发安全搞定了,Go 的基础语法和并发编程就基本学完了。但写程序不只是写代码,还要把代码组织好。接下来看 Go 包,学习怎么用包把代码按功能分组管理。