Golang基础笔记十五之sync
在并发编程的世界里,Golang 的 goroutine 像一群不知疲倦的小精灵,各自独立地执行任务。但当多个 goroutine 同时访问共享资源时,就会引发数据竞争(data race) ,就像一群人同时抢一个麦克风,场面就会失控。今天我们要聊的 sync 包,就是 Golang 官方提供的"秩序维护者",它帮我们优雅地解决并发协作问题。### 为什么需要 sync?先看一个反面教材。假设我们要统计一段文本中单词的总数,用多个 goroutine 分别统计不同段落,最后汇总。如果直接用一个共享变量累加,会发生什么?gopackage mainimport ( "fmt" "runtime" "sync")func main() { var counter int var wg sync.WaitGroup // 模拟 10 个 goroutine 同时写 counter for i := 0; i < 10; i++ { wg.Add(1) go func() { defer wg.Done() for j := 0; j < 1000; j++ { counter++ // 这里就是数据竞争点 } }() } wg.Wait() fmt.Println("最终计数:", counter)}运行这段代码,你会发现结果几乎不可能是 10000(正确值)。因为 counter++ 不是原子操作,它分为"读取-修改-写入"三步,多个 goroutine 同时执行时,会互相覆盖对方的操作。此时,sync 包就派上用场了。### 一、sync.Mutex ------ 互斥锁Mutex 是最基础的同步原语,它确保同一时刻只有一个 goroutine 能访问临界区 。就像厕所的门锁,谁拿到钥匙谁进入,用完再还回来。gopackage mainimport ( "fmt" "sync")func main() { var mu sync.Mutex // 定义互斥锁 var counter int var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) go func() { defer wg.Done() for j := 0; j < 1000; j++ { mu.Lock() // 加锁 counter++ // 临界区:同一时间只有一个 goroutine 能执行这里 mu.Unlock() // 解锁 } }() } wg.Wait() fmt.Println("最终计数:", counter) // 确保输出 10000}关键点 :- Lock() 和 Unlock() 必须成对出现,否则会死锁。建议用 defer 确保解锁。- 锁的粒度要小,避免长时间持有锁导致其他 goroutine 阻塞。### 二、sync.RWMutex ------ 读写锁如果场景是"读多写少",比如缓存系统,用 Mutex 会浪费性能------因为多个读操作其实可以并发进行。RWMutex 提供了更细致的控制:读锁可共享,写锁独占 。gopackage mainimport ( "fmt" "sync" "time")type Cache struct { mu sync.RWMutex items map[string]int}func NewCache() *Cache { return &Cache{items: make(map[string]int)}}// 读操作:加读锁,多个读者可同时进入func (c *Cache) Get(key string) (int, bool) { c.mu.RLock() defer c.mu.RUnlock() val, ok := c.items[key] return val, ok}// 写操作:加写锁,独占访问func (c *Cache) Set(key string, val int) { c.mu.Lock() defer c.mu.Unlock() c.items[key] = val}func main() { cache := NewCache() cache.Set("apple", 5) var wg sync.WaitGroup // 模拟多个读者并发读 for i := 0; i < 5; i++ { wg.Add(1) go func() { defer wg.Done() for j := 0; j < 100; j++ { if v, ok := cache.Get("apple"); ok && v == 5 { // 读操作成功 } } }() } wg.Wait() fmt.Println("并发读取完成")}注意 :读锁会阻塞写锁,写锁会阻塞所有读锁。如果写操作特别频繁,RWMutex 可能反而更慢。### 三、sync.WaitGroup ------ 等待组WaitGroup 用于等待一组 goroutine 全部完成任务 。它内部维护一个计数器,Add(n) 增加计数,Done() 减少计数,Wait() 阻塞直到计数归零。gopackage mainimport ( "fmt" "sync" "time")func worker(id int) { defer fmt.Printf("worker %d 完成\n", id) time.Sleep(time.Second)}func main() { var wg sync.WaitGroup for i := 1; i <= 3; i++ { wg.Add(1) // 每启动一个 goroutine 就 +1 go func(id int) { defer wg.Done() // 完成后 -1 worker(id) }(i) } wg.Wait() // 阻塞直到计数器为 0 fmt.Println("所有 worker 执行完毕")}易错点 :Add 必须在 go 语句之前调用,否则可能提前 Wait 结束。### 四、sync.Once ------ 只执行一次Once 用于确保某个函数在整个程序生命周期中只执行一次 ,非常适合初始化全局资源(如数据库连接)。gopackage mainimport ( "fmt" "sync")var ( configOnce sync.Once config string)func loadConfig() { configOnce.Do(func() { // 模拟耗时初始化 config = "从配置文件加载的数据" fmt.Println("初始化完成") })}func main() { // 多个 goroutine 并发调用 loadConfig var wg sync.WaitGroup for i := 0; i < 3; i++ { wg.Add(1) go func() { defer wg.Done() loadConfig() }() } wg.Wait() fmt.Println("最终配置:", config)}输出只有一次"初始化完成",说明 Do 内的代码只执行了一次。### 五、sync.Map ------ 并发安全的 mapGo 内置的 map 不是并发安全的。sync.Map 专门为"读多写少"的场景优化,它内部使用了读写锁和原子操作,性能通常优于"Mutex + map"。gopackage mainimport ( "fmt" "sync")func main() { var m sync.Map // 写入 m.Store("name", "Alice") m.Store("age", 30) // 读取 if val, ok := m.Load("name"); ok { fmt.Println("name:", val) } // 遍历 m.Range(func(key, value interface{}) bool { fmt.Printf("%v: %v\n", key, value) return true // 返回 false 可停止遍历 }) // 删除 m.Delete("age")}使用建议 :如果 key 是稳定的字符串且读多写少,sync.Map 很合适;如果写操作频繁,还是用 Mutex + map 更简单可控。### 六、sync.Pool ------ 对象池Pool 用于缓存临时对象 ,减少 GC 压力和分配开销。它适合存储"创建成本高、可复用"的对象(如缓冲区)。gopackage mainimport ( "fmt" "sync")type Buffer struct { data []byte}func main() { pool := sync.Pool{ New: func() interface{} { return &Buffer{data: make([]byte, 1024)} }, } // 获取对象 buf := pool.Get().(*Buffer) buf.data[0] = 'A' fmt.Println("使用中:", buf.data[0]) // 放回池中 pool.Put(buf) // 再次获取,可能得到同一个对象 buf2 := pool.Get().(*Buffer) fmt.Println("再次获取:", buf2.data[0]) // 输出 'A'(如果被复用了)}注意 :Pool 中的对象可能被 GC 清除,不要依赖它做持久化存储。### 总结sync 包是 Go 并发编程的基石,它提供了从"互斥"到"协作"的完整工具链:- Mutex / RWMutex :解决资源竞争,保护临界区。- WaitGroup :等待一组任务完成,实现"分而治之"。- Once :保证只执行一次的初始化逻辑。- Map :开箱即用的并发安全 map。- Pool :对象复用,提升性能。核心原则 :能用 channel 解决的并发问题,优先用 channel;必须共享内存时,再考虑 sync。而且,锁的粒度要小,避免死锁,尽量用 defer 保证解锁。掌握这些工具,你就能像指挥家一样,让 goroutine 们和谐共舞。