学习目标
- 理解为什么 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 并发的“标配”