Go入门到精通-22-同步原语

目录

  • [🟡 Go入门到精通 | 同步原语:Go并发的工具箱](#🟡 Go入门到精通 | 同步原语:Go并发的工具箱)
    • [🔒 为什么需要同步原语](#🔒 为什么需要同步原语)
    • [🔐 sync.Mutex:互斥锁](#🔐 sync.Mutex:互斥锁)
      • 基本用法
      • [⚡ defer 解锁防止死锁](#⚡ defer 解锁防止死锁)
      • [Mutex 使用准则表](#Mutex 使用准则表)
      • [不可复制(Go vet 检查)](#不可复制(Go vet 检查))
      • 锁粒度优化示例
    • [📖 sync.RWMutex:读写锁](#📖 sync.RWMutex:读写锁)
    • [👥 sync.WaitGroup:等待组](#👥 sync.WaitGroup:等待组)
      • 基础模式
      • [⚠️ 经典陷阱:闭包变量捕获](#⚠️ 经典陷阱:闭包变量捕获)
      • [⚠️ Add 位置错误](#⚠️ Add 位置错误)
      • 错误收集模式
    • [1️⃣ sync.Once:只执行一次](#1️⃣ sync.Once:只执行一次)
    • [📢 sync.Cond:条件变量](#📢 sync.Cond:条件变量)
      • 基本模式
      • [Signal vs Broadcast](#Signal vs Broadcast)
      • [⚠️ Cond 使用注意事项](#⚠️ Cond 使用注意事项)
    • [⚛️ atomic:原子操作](#⚛️ atomic:原子操作)
      • 常用原子操作
      • [性能对比:atomic vs Mutex](#性能对比:atomic vs Mutex)
      • [CAS 实现无锁栈](#CAS 实现无锁栈)
      • [atomic vs Mutex 选型指南](#atomic vs Mutex 选型指南)
    • [♻️ sync.Pool:临时对象池](#♻️ sync.Pool:临时对象池)
      • 基本用法
      • [实战:JSON 编码优化](#实战:JSON 编码优化)
      • [⚠️ sync.Pool 注意事项](#⚠️ sync.Pool 注意事项)
    • [🔗 errgroup:并发错误处理](#🔗 errgroup:并发错误处理)
    • [📊 选型指南与最佳实践](#📊 选型指南与最佳实践)
    • [💬 小结与互动](#💬 小结与互动)
      • [🎯 速查表](#🎯 速查表)
      • [💬 思考题](#💬 思考题)

🟡 Go入门到精通 | 同步原语:Go并发的工具箱

📅 更新于 2026年7月 | ✍️ 原创文章,转载请注明出处


🔒 为什么需要同步原语

💡 Channel 不是银弹。

虽然 Go 社区推崇 "不要通过共享内存来通信,而要通过通信来共享内存",但现实编程中,许多场景天然适合用锁和同步原语------比如保护一个被多个 Goroutine 读写的共享数据结构。

本文系统梳理 Go 1.26 中所有核心同步原语,帮你选对工具,少写 Bug


🔐 sync.Mutex:互斥锁

基本用法

go 复制代码
var mu sync.Mutex
var counter int

func increment() {
    mu.Lock()
    counter++
    mu.Unlock()
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            increment()
        }()
    }
    wg.Wait()
    fmt.Println("最终计数:", counter) // 1000
}

⚡ defer 解锁防止死锁

go 复制代码
// ✅ 推荐:defer 保证即使 panic 也能解锁
func safeIncrement() {
    mu.Lock()
    defer mu.Unlock() // 离开函数前自动解锁
    counter++
    // ... 复杂逻辑,可能提前 return 或 panic
}

// ❌ 危险:忘记解锁导致死锁
func dangerousIncrement() {
    mu.Lock()
    if someCondition {
        return // 忘记 Unlock!死锁!
    }
    counter++
    mu.Unlock()
}

Mutex 使用准则表

规则 说明 示例
🔒 Lock/Unlock 成对 一次 Lock 必须对应一次 Unlock defer mu.Unlock()
🚫 不可复制 Mutex 复制后状态分离,锁失效 用指针传递 *sync.Mutex
📦 嵌入结构体 非导出字段避免外部误操作 type SafeMap struct { mu sync.Mutex; data map[string]int }
⏱️ 锁粒度 锁住最小必要范围 锁外处理耗时 IO

不可复制(Go vet 检查)

go 复制代码
type Counter struct {
    mu    sync.Mutex
    value int
}

// ❌ 值传递会复制 Mutex
func (c Counter) Bad() { // 值接收者!
    c.mu.Lock()
    c.value++
    c.mu.Unlock()
} // 实际上操作的是一份副本,原对象根本没锁住

// ✅ 必须用指针接收者
func (c *Counter) Good() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.value++
}

锁粒度优化示例

go 复制代码
// ❌ 锁粒度过大:IO 操作在锁内
func (c *Counter) BadIncrement() {
    c.mu.Lock()
    defer c.mu.Unlock()
    result := expensiveAPICall() // 耗时操作在锁内!
    c.value += result
}

// ✅ 细粒度锁:只在必要时加锁
func (c *Counter) GoodIncrement() {
    result := expensiveAPICall() // 锁外完成
    c.mu.Lock()
    c.value += result
    c.mu.Unlock()
}

📖 sync.RWMutex:读写锁

读写锁是互斥锁的升级版:读操作不互斥,写操作互斥一切

锁的互斥关系矩阵

🔓 无锁 📖 读锁 ✍️ 写锁
📖 加读锁 ✅ 成功 ✅ 成功 ❌ 阻塞
✍️ 加写锁 ✅ 成功 ❌ 阻塞 ❌ 阻塞

基本用法

go 复制代码
type Cache struct {
    mu   sync.RWMutex
    data map[string]string
}

func (c *Cache) Get(key string) (string, bool) {
    c.mu.RLock()         // 读锁------多个 goroutine 可同时持有
    defer c.mu.RUnlock()
    v, ok := c.data[key]
    return v, ok
}

func (c *Cache) Set(key, value string) {
    c.mu.Lock()          // 写锁------排他
    defer c.mu.Unlock()
    c.data[key] = value
}

性能对比:读多写少场景

go 复制代码
func benchmarkReadHeavy() {
    cache := &Cache{data: make(map[string]string)}
    cache.Set("key", "value")

    var wg sync.WaitGroup

    // 100 个并发读
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for j := 0; j < 10000; j++ {
                cache.Get("key")
            }
        }()
    }

    // 1 个写
    wg.Add(1)
    go func() {
        defer wg.Done()
        for j := 0; j < 100; j++ {
            cache.Set("key", "new-value")
        }
    }()

    wg.Wait()
}

🚀 结论 :在读写比 1000:1 的场景下,RWMutexMutex 性能高 10~50 倍

RWMutex 陷阱

go 复制代码
// ⚠️ 锁升级:持有读锁时不能加写锁(死锁!)
func (c *Cache) promote(key string) {
    c.mu.RLock()
    if c.data[key] == "old" {
        c.mu.Lock() // ❌ 死锁!已持有读锁,无法升级为写锁
        c.data[key] = "new"
        c.mu.Unlock()
    }
    c.mu.RUnlock()
}

// ✅ 正确做法:先释放读锁,再加写锁
func (c *Cache) promoteCorrect(key string) {
    c.mu.RLock()
    needUpdate := c.data[key] == "old"
    c.mu.RUnlock()

    if needUpdate {
        c.mu.Lock()
        c.data[key] = "new"
        c.mu.Unlock()
    }
}

👥 sync.WaitGroup:等待组

协调一组 Goroutine 完成:Add 计数器 +1,Done 计数器 -1,Wait 阻塞直到计数器归零。

基础模式

go 复制代码
func main() {
    var wg sync.WaitGroup
    urls := []string{"url1", "url2", "url3", "url4", "url5"}

    for _, url := range urls {
        wg.Add(1) // 计数器 +1
        go func(u string) {
            defer wg.Done() // 计数器 -1
            fetch(u)
        }(url) // ⚠️ 注意:必须传参或使用局部变量
    }

    wg.Wait() // 阻塞直到计数器归零
    fmt.Println("所有任务完成")
}

⚠️ 经典陷阱:闭包变量捕获

go 复制代码
// ❌ 错误:所有 goroutine 都用同一个 url 变量
for _, url := range urls {
    wg.Add(1)
    go func() {
        defer wg.Done()
        fetch(url) // 所有 goroutine 看到的 url 是最后一次的值!
    }()
}

// ✅ 方案 1:传参
go func(u string) {
    defer wg.Done()
    fetch(u)
}(url)

// ✅ 方案 2:局部变量遮蔽
url := url // 在 goroutine 启动前创建副本
go func() {
    defer wg.Done()
    fetch(url)
}()

⚠️ Add 位置错误

go 复制代码
// ❌ Add 在 goroutine 内部------可能 Wait 先于 Add 执行
go func() {
    wg.Add(1)  // 太晚了!
    defer wg.Done()
    doWork()
}()

// ✅ Add 在 goroutine 启动前
wg.Add(1)
go func() {
    defer wg.Done()
    doWork()
}()

错误收集模式

go 复制代码
func parallelFetch(urls []string) []error {
    var wg sync.WaitGroup
    errCh := make(chan error, len(urls))

    for _, url := range urls {
        wg.Add(1)
        go func(u string) {
            defer wg.Done()
            if err := fetch(u); err != nil {
                errCh <- err
            }
        }(url)
    }

    go func() {
        wg.Wait()
        close(errCh)
    }()

    var errs []error
    for err := range errCh {
        errs = append(errs, err)
    }
    return errs
}

1️⃣ sync.Once:只执行一次

单例模式的标准实现

go 复制代码
type Config struct {
    DBHost string
    DBPort int
}

var (
    instance *Config
    once     sync.Once
)

func GetConfig() *Config {
    once.Do(func() {
        // 这段代码只会执行一次,即使多个 goroutine 同时调用
        fmt.Println("初始化配置...")
        instance = &Config{
            DBHost: "localhost",
            DBPort: 5432,
        }
    })
    return instance
}

func main() {
    // 100 个 goroutine 同时获取配置
    for i := 0; i < 100; i++ {
        go func() {
            cfg := GetConfig()
            fmt.Println(cfg.DBHost)
        }()
    }
}

Once 的执行语义

特性 说明
🔒 线程安全 多个 goroutine 同时调用 Do(),只有一个执行
🚫 不可重复 Do() 中的函数只执行一次,后续调用直接返回
⚠️ panic 不影响 函数 panic 后 Once 仍认为已执行,不会重试
📦 不可复制 同 Mutex,必须用指针

💡 使用场景:初始化数据库连接池、加载配置文件、注册全局组件。


📢 sync.Cond:条件变量

sync.Cond 是 Go 中最少被提及但非常强大的同步原语,用于等待某个条件满足后唤醒等待的 goroutine

基本模式

go 复制代码
type Queue struct {
    mu    sync.Mutex
    cond  *sync.Cond
    items []int
    cap   int
}

func NewQueue(cap int) *Queue {
    q := &Queue{cap: cap}
    q.cond = sync.NewCond(&q.mu)
    return q
}

// 生产者:队列满时等待
func (q *Queue) Produce(item int) {
    q.mu.Lock()
    for len(q.items) == q.cap { // ⚠️ 必须用 for 而非 if
        q.cond.Wait() // 等待消费者唤醒
    }
    q.items = append(q.items, item)
    fmt.Println("生产:", item)
    q.cond.Signal() // 唤醒一个等待的消费者
    q.mu.Unlock()
}

// 消费者:队列空时等待
func (q *Queue) Consume() int {
    q.mu.Lock()
    for len(q.items) == 0 {
        q.cond.Wait() // 等待生产者唤醒
    }
    item := q.items[0]
    q.items = q.items[1:]
    fmt.Println("消费:", item)
    q.cond.Signal()
    q.mu.Unlock()
    return item
}

Signal vs Broadcast

方法 效果 适用场景
🔔 Signal() 唤醒一个等待的 goroutine 每次只需一个 goroutine 处理
📢 Broadcast() 唤醒所有等待的 goroutine 全局状态变更,所有等待者都应重新检查
go 复制代码
// Broadcast 示例:配置热更新
type HotConfig struct {
    mu   sync.Mutex
    cond *sync.Cond
    data map[string]string
}

func (hc *HotConfig) Reload(newData map[string]string) {
    hc.mu.Lock()
    hc.data = newData
    hc.mu.Unlock()
    hc.cond.Broadcast() // 通知所有等待者配置已更新
}

func (hc *HotConfig) WaitForUpdate(oldVersion int) map[string]string {
    hc.mu.Lock()
    hc.cond.Wait() // 等待配置更新
    data := hc.data
    hc.mu.Unlock()
    return data
}

⚠️ Cond 使用注意事项

  1. 必须用 for 而非 if:Wait() 可能被虚假唤醒(spurious wakeup)
  2. Wait() 前必须持有锁:Wait() 内部会释放锁然后阻塞,被唤醒后重新获取锁
  3. Signal 不保证顺序:FIFO 不保证,只保证唤醒一个

⚛️ atomic:原子操作

对于简单的计数器、标志位,sync/atomic 比 Mutex 轻量得多。

常用原子操作

go 复制代码
import "sync/atomic"

// 原子加法
var counter int64
atomic.AddInt64(&counter, 1)     // counter += 1
atomic.AddInt64(&counter, -1)    // counter -= 1

// 原子读取 & 存储
var flag int32
atomic.StoreInt32(&flag, 1)       // flag = 1
val := atomic.LoadInt32(&flag)    // 读取 flag

// CAS (Compare And Swap):无锁编程的核心
// 如果 addr 的值等于 old,则设为 new,返回是否成功
var state int64
swapped := atomic.CompareAndSwapInt64(&state, 0, 1)

// 原子值:存储任意类型
var config atomic.Value
config.Store(&Config{DBHost: "localhost"})
cfg := config.Load().(*Config)

性能对比:atomic vs Mutex

go 复制代码
// atomic 实现计数器
func atomicCounter() {
    var counter int64
    var wg sync.WaitGroup
    for i := 0; i < 10000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            atomic.AddInt64(&counter, 1)
        }()
    }
    wg.Wait()
}

// Mutex 实现计数器
func mutexCounter() {
    var mu sync.Mutex
    var counter int64
    var wg sync.WaitGroup
    for i := 0; i < 10000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            mu.Lock()
            counter++
            mu.Unlock()
        }()
    }
    wg.Wait()
}
// atomic 版本快 2~5 倍(竞争越激烈差距越大)

CAS 实现无锁栈

go 复制代码
type LockFreeStack struct {
    top unsafe.Pointer // *node
}

type node struct {
    value int
    next  unsafe.Pointer // *node
}

func (s *LockFreeStack) Push(v int) {
    n := &node{value: v}
    for {
        oldTop := atomic.LoadPointer(&s.top)
        n.next = oldTop
        if atomic.CompareAndSwapPointer(&s.top, oldTop, unsafe.Pointer(n)) {
            return
        }
        // CAS 失败,说明被其他 goroutine 抢先了,重试
    }
}

func (s *LockFreeStack) Pop() (int, bool) {
    for {
        oldTop := atomic.LoadPointer(&s.top)
        if oldTop == nil {
            return 0, false
        }
        next := (*node)(oldTop).next
        if atomic.CompareAndSwapPointer(&s.top, oldTop, next) {
            return (*node)(oldTop).value, true
        }
    }
}

atomic vs Mutex 选型指南

场景 推荐 原因
🔢 简单计数器 atomic 极快,无锁
🚩 布尔标志位 atomic 一条指令搞定
🔄 复杂数据结构 Mutex 多个字段需要一致性
📊 高频读写 atomicRWMutex 看读写比例

♻️ sync.Pool:临时对象池

复用高频创建销毁的对象,减少 GC 压力。

基本用法

go 复制代码
var bufferPool = sync.Pool{
    New: func() interface{} {
        return make([]byte, 4096) // 池子空时创建新的
    },
}

func process(data []byte) {
    buf := bufferPool.Get().([]byte)
    defer bufferPool.Put(buf) // 用完归还

    // 使用 buf 处理...
    copy(buf, data)
    doSomething(buf)
}

实战:JSON 编码优化

go 复制代码
var encoderPool = sync.Pool{
    New: func() interface{} {
        return json.NewEncoder(nil)
    },
}

func encode(w io.Writer, v interface{}) error {
    enc := encoderPool.Get().(*json.Encoder)
    defer encoderPool.Put(enc)

    // 重置 encoder(Go 1.26 无 Reset 方法时需重新创建 writer)
    // 可以在自定义 wrapper 中实现
    return enc.Encode(v)
}

⚠️ sync.Pool 注意事项

注意点 说明
🗑️ GC 会清空池 Pool 中的对象会在 GC 时被回收,不能依赖池中永远有对象
📏 对象大小 适合临时对象,不适合持久化数据
生命周期 每次 Get 后必须 Put,不然 Pool 失去意义

🔗 errgroup:并发错误处理

golang.org/x/sync/errgroup 提供了WaitGroup + 错误传播的能力。

基础用法

go 复制代码
import "golang.org/x/sync/errgroup"

func fetchAll(urls []string) error {
    g := new(errgroup.Group)

    for _, url := range urls {
        url := url // 闭包变量捕获
        g.Go(func() error {
            resp, err := http.Get(url)
            if err != nil {
                return fmt.Errorf("请求 %s 失败: %w", url, err)
            }
            defer resp.Body.Close()
            // 处理响应...
            return nil
        })
    }

    // Wait 返回第一个错误,同时取消其他 goroutine
    if err := g.Wait(); err != nil {
        return fmt.Errorf("批量请求失败: %w", err)
    }
    return nil
}

WithContext:级联取消

go 复制代码
func fetchAllWithCancel(ctx context.Context, urls []string) error {
    g, ctx := errgroup.WithContext(ctx)

    for _, url := range urls {
        url := url
        g.Go(func() error {
            req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
            resp, err := http.DefaultClient.Do(req)
            if err != nil {
                return err
            }
            defer resp.Body.Close()
            return nil
        })
    }

    return g.Wait()
    // 任何一个 goroutine 返回错误,ctx 被取消,
    // 其他 goroutine 的 HTTP 请求也会终止
}

限制并发数

go 复制代码
func fetchAllLimited(urls []string, limit int) error {
    g := new(errgroup.Group)
    g.SetLimit(limit) // Go 1.20+ 支持,限制同时运行的 goroutine 数

    for _, url := range urls {
        url := url
        g.Go(func() error {
            return fetch(url)
        })
    }
    return g.Wait()
}

errgroup 错误处理模式

go 复制代码
// 收集所有错误而非只取第一个
func fetchAllCollectErrors(urls []string) []error {
    g := new(errgroup.Group)
    errMu := sync.Mutex{}
    var errs []error

    for _, url := range urls {
        url := url
        g.Go(func() error {
            if err := fetch(url); err != nil {
                errMu.Lock()
                errs = append(errs, err)
                errMu.Unlock()
            }
            return nil // 不返回错误,继续执行其他任务
        })
    }

    g.Wait()
    return errs
}

📊 选型指南与最佳实践

同步原语选型决策树

复制代码
需要协调多个 Goroutine?
├── 等待所有 Goroutine 完成?
│   └── ✅ sync.WaitGroup
├── 保护共享数据?
│   ├── 简单计数器/标志位?
│   │   └── ✅ atomic
│   ├── 读多写少?
│   │   └── ✅ sync.RWMutex
│   └── 读写均衡 + 逻辑复杂?
│       └── ✅ sync.Mutex
├── 等待条件满足?
│   └── ✅ sync.Cond
├── 只初始化一次?
│   └── ✅ sync.Once
├── 并发任务 + 错误传播?
│   └── ✅ errgroup
└── 复用对象减少 GC?
    └── ✅ sync.Pool

最佳实践清单

✅ 实践 说明
🔒 defer mu.Unlock() 防止忘记解锁
📍 锁粒度最小化 IO 操作不要放锁内
🚫 不可复制锁 用指针或嵌入结构体
⚠️ WaitGroup.Add 在 goroutine 外 避免竞态
♻️ Pool.Put 后不访问 对象随时可能被其他 goroutine 取走
🔄 RWMutex 不可升级 释放读锁后才能加写锁

💬 小结与互动

🎯 速查表

原语 核心功能 经典场景
sync.Mutex 互斥锁 保护共享数据结构
sync.RWMutex 读写锁 读多写少缓存
sync.WaitGroup 等待组 并发任务协调
sync.Once 一次执行 单例初始化
sync.Cond 条件变量 生产者-消费者
atomic 原子操作 计数器、标志位
sync.Pool 对象池 高频临时对象复用
errgroup 错误传播 批量并发任务

💬 思考题

  1. 以下代码有什么隐患?
go 复制代码
var mu sync.Mutex
func update(m map[string]int) {
    mu.Lock()
    m["count"]++
    mu.Unlock()
}

👆 点击查看答案 `update` 接受的是 map 的引用,没问题。但如果有其他地方直接访问这个 map 而没有先加锁,就会 data race。应该将所有对 map 的访问封装到带锁的方法中。

  1. 为什么 sync.Cond.Wait() 必须放在 for 循环里?

👆 点击查看答案 两个原因:(1) 虚假唤醒------操作系统可能错误唤醒等待的线程;(2) Broadcast 唤醒所有等待者,但可能只有一个能满足条件,其他需要继续等待。


📖 下一篇预告:Pipeline 管道模式、Generator 生成器、生产者-消费者...Go 并发模式(上)!


作者:布朗克168 | 系列:Go入门到精通 2026

相关推荐
云上小朱18 小时前
在docker环境部署Alist3.62.0
后端
wei_shuo18 小时前
KES 事务管理与MVCC深度解析:隔离级别、快照机制与并发控制
后端
程序员清风18 小时前
公司要裁员之前,都会有哪些信号?
java·后端·面试
帅次18 小时前
Kotlin Flow 与 StateFlow:UI 单向数据流基础
开发语言·ui·kotlin·stateflow·onlifecycle
AskHarries18 小时前
Stripe 集成教程
后端
65岁退休Coder18 小时前
LangChain v1.3.4 笔记 - 03 Agent 的 model、tools、response_format 及 stream 输出
后端
艾伦野鸽ggg18 小时前
JavaScript 浏览器本地存储
开发语言·javascript·ecmascript
饼干哥哥18 小时前
n8n 又活了?用 Codex把跨境电商工作流转成 Skill
人工智能·后端·代码规范
武子康18 小时前
Java 后端 → 实时语音 AI 转型复盘:4 个 FDE 技术底座 + 6 个差距 + 6 类职业资产路线
人工智能·后端·openai