sync 包

当多个 goroutine 需要访问共享状态时,必须使用同步原语避免数据竞争。sync 包提供了标准工具集。

竞争问题的演示

package main

import (
    "fmt"
    "sync"
)

func main() {
    var count int
    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
}

使用 go run -race main.go 运行上述代码,竞态检测器会明确报告竞争位置。

sync.Mutex:互斥锁

package main

import (
    "fmt"
    "sync"
)

type Counter struct {
    mu sync.Mutex
    n  map[string]int
}

func NewCounter() *Counter {
    return &Counter{n: make(map[string]int)}
}

func (c *Counter) Inc(key string) {
    c.mu.Lock()
    defer c.mu.Unlock() // defer 解锁,任何退出路径都不遗漏
    c.n[key]++
}

func (c *Counter) Get(key string) int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.n[key]
}

func main() {
    c := NewCounter()
    var wg sync.WaitGroup
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            c.Inc("hits")
        }()
    }
    wg.Wait()
    fmt.Println(c.Get("hits")) // 稳定输出 1000
}
Tip

锁的粒度应尽量小:只在临界区内访问共享数据,I/O、网络调用与耗时计算放在锁外。持锁期间调用未知代码(回调等)容易引发死锁。

sync.RWMutex:读写锁

读多写少的场景使用读写锁,允许多个读操作并行:

type Config struct {
    mu   sync.RWMutex
    data map[string]string
}

func (c *Config) Get(key string) string {
    c.mu.RLock()          // 读锁:多个读者可同时持有
    defer c.mu.RUnlock()
    return c.data[key]
}

func (c *Config) Set(key, val string) {
    c.mu.Lock()           // 写锁:独占
    defer c.mu.Unlock()
    c.data[key] = val
}

sync.Once:单次执行

保证某段逻辑在并发环境下只执行一次,典型场景是单例初始化:

package main

import (
    "fmt"
    "sync"
)

var (
    instance *Database
    once     sync.Once
)

type Database struct{ Conn string }

func GetDB() *Database {
    once.Do(func() { // 首次调用时执行且仅执行一次
        instance = &Database{Conn: "localhost:5432"}
    })
    return instance
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            fmt.Println(GetDB().Conn)
        }()
    }
    wg.Wait()
}

sync.Map:并发安全的映射

sync.Map 针对读多写少、键集合稳定的场景做了专门优化,且零值即可使用:

package main

import (
    "fmt"
    "sync"
)

func main() {
    var m sync.Map

    var wg sync.WaitGroup
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            m.Store("key", 1) // 并发写不会竞争
        }()
    }
    wg.Wait()

    if v, ok := m.Load("key"); ok {
        fmt.Println(v.(int)) // 100(最终值)
    }

    m.Delete("key")
    m.Range(func(k, v any) bool { // 遍历
        fmt.Println(k, v)
        return true
    })
}
Tip

sync.Map 的类型为 map[any]any,缺乏编译期类型检查,读写性能在写多场景下不如 Mutex + map。一般业务代码优先使用 Mutex + map[K]V(可配合泛型封装);仅在缓存、读密集等特定负载下使用 sync.Map

原子操作:sync/atomic

对单一数值的增减与交换,原子操作比加锁开销更低:

package main

import (
    "fmt"
    "sync"
    "sync/atomic"
)

func main() {
    var total int64
    var wg sync.WaitGroup

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            atomic.AddInt64(&total, 1) // 原子自增
        }()
    }
    wg.Wait()

    fmt.Println(atomic.LoadInt64(&total)) // 1000
}

Go 1.19 起提供泛型形式 atomic.Int64atomic.Pointer[T] 等类型,进一步简化使用:

var counter atomic.Int64
counter.Add(1)
_ = counter.Load()

小结

  • 共享状态必须保护:Mutex 通用,RWMutex 优化读多写少。
  • sync.Once 实现并发安全的单次初始化。
  • sync.Mapsync/atomic 适用于特定负载;通用场景优先 Mutex + map