Day 14|sync 包与互斥原语(Mutex / RWMutex / Once)[Go 语言 30 天系统学习计划]

学习目标

  • 理解为什么 Go 在 channel 之外仍然需要锁
  • 掌握 Mutex 与 RWMutex 的正确使用方式
  • 理解 Once 的真实语义与工程用途
  • 学会在 channel 与 mutex 之间做工程取舍

一、为什么仍然需要锁(mutex)

Go 的并发哲学是:

通过通信来共享数据,而不是通过共享数据来通信

但这并不意味着:

  • 锁是“反 Go 的”
  • channel 可以替代所有并发控制

现实工程中:

  • 大量共享状态(map、计数器、缓存)
  • 频繁读写、简单临界区
  • 对性能与内存敏感

这些场景下,mutex 往往比 channel 更简单、更高效


二、Mutex:最基础的互斥锁

sync.Mutex 用于保护临界区,保证同一时刻只有一个 goroutine 可以访问。

基本用法:

type Counter struct {
    mu sync.Mutex
    n  int
}

func (c *Counter) Inc() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.n++
}

func (c *Counter) Value() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.n
}

关键原则:

  • Lock 和 Unlock 必须成对出现
  • 推荐用 defer 解锁,避免遗漏
  • 锁保护的是“共享状态”,不是函数本身

三、Mutex 的常见错误

错误 1:忘记 Unlock(死锁)

mu.Lock()
// return 或 panic,未 Unlock

错误 2:重复加锁(同一 goroutine)

mu.Lock()
mu.Lock() // 会永久阻塞(Go 的 Mutex 不是可重入锁)

错误 3:复制含有 Mutex 的结构体

type S struct {
    mu sync.Mutex
}

s1 := S{}
s2 := s1 // 错误:复制了锁

工程规则:含有 mutex 的 struct 一律用指针传递。


四、RWMutex:读写锁

sync.RWMutex 用于读多写少的场景。

  • 多个 goroutine 可以同时读
  • 写操作是独占的

基本用法:

type Store struct {
    mu sync.RWMutex
    m  map[string]int
}

func (s *Store) Get(key string) int {
    s.mu.RLock()
    defer s.mu.RUnlock()
    return s.m[key]
}

func (s *Store) Set(key string, val int) {
    s.mu.Lock()
    defer s.mu.Unlock()
    s.m[key] = val
}

工程注意:

  • 读锁不能升级为写锁
  • 写锁会阻塞所有读锁
  • 不要在持锁期间做耗时操作

五、Once:只执行一次的并发原语

sync.Once 用于保证某段代码在并发场景下只执行一次

典型用途:

  • 单例初始化
  • 全局资源初始化
  • 惰性加载

示例:

var once sync.Once

func initConfig() {
    fmt.Println("init config")
}

func LoadConfig() {
    once.Do(initConfig)
}

即使多个 goroutine 同时调用 LoadConfig

  • initConfig 只会执行一次
  • 其余 goroutine 会等待执行完成

六、Once 的重要语义细节

  • Once.Do 只关心“是否执行过”,不关心执行结果
  • 如果函数 panic,Once 仍然认为“已经执行过”
  • Once 不能重置

工程结论:

Once 适合“必须成功一次”的初始化,不适合“可能失败重试”的逻辑。


七、channel vs mutex:如何选择

优先使用 mutex 的场景:

  • 保护共享内存(map、struct)
  • 简单临界区
  • 高频读写

优先使用 channel 的场景:

  • 任务协作、流水线
  • 事件通知
  • goroutine 生命周期管理

工程共识:

mutex 是“保护状态”,channel 是“协调行为”。


八、sync 包的工程定位总结

  • Mutex:最基础、最常用的互斥原语
  • RWMutex:读多写少时的性能优化工具
  • Once:并发安全的一次性初始化

一句话总结:

channel 解决协作问题,mutex 解决共享问题,Once 解决初始化问题。


预告:Day 15|context:并发取消、超时与生命周期管理

下一天将系统讲解:

  • context 的设计初衷
  • 取消、超时与截止时间
  • context 在 goroutine 树中的传播
  • 为什么 context 是现代 Go 并发的“标配”

发表评论