sync.mutex需显式加锁解锁,推荐defer确保释放;rwmutex适用于读多写少场景;零值可用但禁止复制已使用mutex;竞态检测不能替代正确并发设计。

sync.Mutex 必须显式加锁和解锁
Go 的 sync.Mutex 不是自动管理的,不加锁就读写共享数据,或者加了锁但忘了 Unlock(),都会立刻出问题——要么数据竞争(go run -race 会报 Data race),要么 goroutine 永久阻塞。
最稳妥的做法是用 defer mu.Unlock() 配合 mu.Lock(),确保函数退出前一定释放:
func (c *Counter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
c.val++
}
- 别在 if 分支里只对部分路径加锁,容易漏掉
Unlock() - 别把
Lock()和Unlock()拆到不同函数里——编译器无法校验配对,极易出错 - 如果需要重入(即同 goroutine 多次加锁),
sync.Mutex不支持,得换sync.RWMutex或自己加计数逻辑
sync.RWMutex 适合读多写少的场景
当结构体被频繁读取、极少修改时,用 sync.RWMutex 能显著提升并发吞吐。它的 RLock() 允许多个 goroutine 同时读,而 Lock() 仍会独占写入。
但注意:一旦有 goroutine 调用了 Lock(),所有新的 RLock() 都会阻塞,直到写操作完成——不是“读优先”,而是“写一来,读就得排队”。
- 写操作前必须用
mu.Lock(),不能误用mu.RLock() -
RLock()后也必须配对mu.RUnlock(),漏掉会导致后续所有写操作永久等待 - 不要在持有
RLock()期间调用可能触发写操作的函数(比如某个回调),否则可能死锁
零值 Mutex 是可用的,但别复制已使用的 Mutex
sync.Mutex 和 sync.RWMutex 的零值本身就是有效且未锁定的状态,所以可以直接声明,不用 new() 或显式初始化:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
type Cache struct {
mu sync.RWMutex
data map[string]string
}
但一旦它被使用过(即调用过 Lock() 或 RLock()),就不能再被复制——包括作为参数传值、赋值给新变量、放进 map 或 slice。Go 1.21+ 的 vet 工具会报 copy of mutex value 警告,运行时也可能 panic。
- 结构体字段用指针接收(如
*Cache)比值接收更安全,避免无意复制 - 如果必须传值,确保传的是指针:
process(&c),而不是process(c) - 切片扩容、map 赋值、json.Unmarshal 等隐式复制操作,都可能触发 Mutex 复制,要特别留心
竞态检测不能代替设计,Mutex 不是万能解药
go run -race 能发现裸写共享变量的问题,但它抓不到逻辑错误:比如两个本该原子执行的操作被拆成两次加锁,中间穿插了其他 goroutine 的修改。
Mutex 只保证临界区串行,不保证业务语义正确。例如银行转账,只锁各自账户还不够,还得约定加锁顺序(如按 ID 升序),否则可能死锁。
- 锁粒度太粗(整个函数都锁)会拖慢性能;太细(每行都锁)又增加复杂度和出错概率
- 有些场景更适合用 channel 通信替代共享内存,比如任务分发、状态通知
- 高频小对象计数,考虑
sync/atomic,它比 Mutex 更轻量,且无锁
真正难的从来不是怎么加锁,而是判断哪里该加、加多大范围、以及加锁之外有没有更干净的并发模型可选。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










