直接用sync.Mutex不能解决所有竞态问题,因存在忘记Unlock致死锁、值传递导致锁失效、锁保护范围不足等常见错误。

为什么直接用 sync.Mutex 不能解决所有竞态问题
很多人以为只要在读写共享变量前加 mu.Lock()、结束后调用 mu.Unlock() 就万事大吉,但实际常踩几个坑:
— 忘记在所有出口路径(比如 return 前或 defer 失效的分支)调用 Unlock(),导致死锁;
— 把 mutex 作为值传递(比如传入函数时没取地址),导致每次操作的其实是副本,完全没上锁;
— 在锁保护范围外修改了本应受保护的字段(比如结构体里某个字段没被锁住,但逻辑上依赖它与其他字段的一致性)。
正确初始化和使用 sync.Mutex 的常见模式
最稳妥的方式是把 sync.Mutex 嵌入结构体,并始终以指针方式访问该结构体:
type Counter struct {
mu sync.Mutex
value int
}
func (c *Counter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}
func (c *Counter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}
注意:
— sync.Mutex 不能复制,所以结构体实例必须用 &Counter{} 创建;
— defer Unlock() 是惯用写法,但仅适用于函数末尾统一释放的场景;若中间有多个 return,仍要确保每个都配对;
— 不要用 sync.RWMutex 替代,除非你明确需要读多写少且读操作不阻塞其他读。
什么时候该换 sync.RWMutex 或更高级方案
当你的共享数据读远多于写,且读操作本身耗时(比如遍历 map 或拼接字符串),sync.RWMutex 能显著提升吞吐:
立即学习“go语言免费学习笔记(深入)”;
-
RLock()和RUnlock()允许多个 goroutine 同时读 - 但一旦有 goroutine 调用
Lock(),所有新进的RLock()都会阻塞,直到写完成 - 注意:
RWMutex不是性能银弹 —— 它内部开销比Mutex大,且写饥饿(writer starvation)在高读负载下可能发生 - 如果共享状态简单(如单个整数),优先考虑
atomic包,比如atomic.AddInt64(&x, 1),它比锁更快、无阻塞
调试竞态问题:别只靠肉眼,用 -race 编译标记
Go 自带竞态检测器,是发现漏锁、误锁最有效的手段:
— 编译时加 -race:go run -race main.go;
— 运行时一旦检测到两个 goroutine 无同步地访问同一内存地址,会立即打印类似这样的错误:
WARNING: DATA RACE
Read at 0x00c000018070 by goroutine 7:
main.(*Counter).Value()
/tmp/main.go:15 +0x39
Previous write at 0x00c000018070 by goroutine 6:
main.(*Counter).Inc()
/tmp/main.go:10 +0x49
— 注意:开启 -race 后程序变慢、内存占用翻倍,**仅用于测试和开发环境**,切勿在生产部署中启用。
真正难缠的不是不会加锁,而是锁的粒度和边界没想清楚 —— 比如该锁整个结构体,还是只锁其中几个字段;该在函数入口锁,还是在具体操作前才锁。这些得结合数据访问模式来判断,没法套模板。










