sync.mutex不能复制,必须用指针接收者、成对调用lock/unlock,并按资源边界细粒度加锁;值拷贝会导致锁失效或panic。

细粒度锁定不是加更多锁,而是让每个锁只管它该管的那一小块数据;用错粒度,性能和正确性都会崩。
sync.Mutex 不能直接嵌入结构体字段再复制
Go 的 sync.Mutex 零值可用,但一旦被使用过,就绝对不能复制——包括作为结构体字段被赋值、传参、返回时的隐式拷贝。常见错误是写成:
type Cache struct {
mu sync.Mutex
data map[string]int
}
func (c Cache) Get(key string) int { // ❌ 值接收者 → 每次调用都拷贝整个 struct,含一个新 mu
c.mu.Lock() // 锁的是副本!原结构体的 mu 根本没被保护
defer c.mu.Unlock()
return c.data[key]
}
正确做法必须用指针接收者 + 显式声明指针类型:
- 始终用
*Cache作方法接收者 - 初始化时用
&Cache{data: make(map[string]int)},而非Cache{...} - 切勿在 map/slice 中存
Cache值,而应存*Cache
按资源边界划分锁,而不是按操作类型
比如你有一个用户会话缓存 map[string]*Session,不要为整个 map 加一把全局 sync.Mutex,也不该为每个 Get/Set 方法各配一把锁。真正合理的做法是:
- 对每个
*Session实例内嵌一个mu sync.Mutex,保护其内部状态(如最后活跃时间、权限令牌) - 对顶层 map 的读写仍需单独一把锁(或改用
sync.RWMutex),但仅用于增删 key,不涉及 Session 内部字段 - 这样并发读多个不同用户 session 完全无竞争,写同一用户 session 才互斥
典型错误是把所有 session 字段都塞进一个大锁里,结果 A 用户更新头像时,B 用户查登录态也被卡住。
Lock/Unlock 必须成对出现在同一栈帧,且 Unlock 不可延迟到 caller
defer mu.Unlock() 看似安全,但在某些场景下会失效:
- 函数有多个 return 路径,但某条路径提前 return 且没走到 defer 行(极少见,但可能因 panic 恢复逻辑出错)
- 锁被跨 goroutine 传递(例如把
mu作为参数发给另一个 goroutine),defer 在原 goroutine 结束时触发,而目标 goroutine 还在等锁 - 更隐蔽的是:在循环中反复
Lock()却只在循环外defer Unlock(),导致锁持有时间远超必要
推荐模式是显式控制生命周期:
mu.Lock() // ... 临界区短小精悍 mu.Unlock()
若确实需要 defer,确保它紧跟在 Lock() 后、且函数内无其他可能提前退出的逻辑分支。
别用 Mutex 保护纯数值型计数器
如果你只是做 counter++ 或 atomic.AddInt64(&x, 1) 这类操作,sync.Mutex 是过度设计。原子操作性能高一个数量级,且无锁竞争开销。
只有当你要保护的是一组关联操作时,Mutex 才不可替代,例如:
- 先查 map 是否存在 key,不存在则插入默认值并初始化子结构
- 从 slice 中 pop 最后一项,同时更新长度字段和容量标记
- 更新 struct 中两个字段,要求它们始终满足某种约束(如
min ≤ value ≤ max)
这些都不是单条 CPU 指令能完成的,必须靠 Mutex 划出临界区。混淆原子操作与临界区边界,是并发 bug 最顽固的来源之一。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











