加了 sync.Mutex 仍有数据竞争,主因是锁未覆盖全部访问路径:读操作未加锁、值类型导致锁被复制、指针未解引用调用 Lock(),或结构体未用指针传递。

为什么加了 sync.Mutex 还有数据竞争?
常见原因是锁没覆盖全部访问路径:比如只在写操作加锁,但读操作直接裸奔;或者锁变量是局部副本(如方法接收者是值类型,mutex 被复制);又或者用了指针但忘记解引用就调用 Lock()。最典型错误是把 sync.Mutex 嵌入结构体后,仍用值拷贝方式传参或赋值,导致每次操作的其实是不同实例的锁。
实操建议:
- 确保所有对共享字段的读写都发生在
mu.Lock()和mu.Unlock()之间 - 结构体必须用指针传递(
*MyStruct),否则嵌入的sync.Mutex不起作用 - 避免在锁内做耗时操作(如 HTTP 请求、文件读写),否则阻塞其他 goroutine
- 用
-race编译并运行程序:go run -race main.go,它能直接报出竞争位置
sync.Mutex 和 sync.RWMutex 怎么选?
当读多写少(比如配置缓存、状态快照),优先用 sync.RWMutex:多个 goroutine 可同时读,但写会独占。而 sync.Mutex 无论读写都互斥,吞吐更低。
注意点:
立即学习“go语言免费学习笔记(深入)”;
-
RWMutex.RLock()和RUnlock()必须成对出现,漏掉会导致后续写操作永久阻塞 -
RWMutex.Lock()会等待所有当前读锁释放,但已有写锁未释放时,新读锁会被阻塞——这可能导致写饥饿(大量读请求压住写) - 如果读操作本身很轻(只是取一个 int 字段),用
sync.Mutex更简单、无额外开销
别在 defer 里无条件调用 Unlock() 的坑
看起来优雅,但容易出错:比如函数提前 return,defer Unlock() 没执行;或者 Lock() 失败(虽然 sync.Mutex 的 Lock() 不会失败,但换成 TryLock() 就可能);更危险的是,在循环中重复 defer Unlock(),造成多次解锁 panic。
安全写法:
- 用
if mu.TryLock() { defer mu.Unlock() }配合显式控制流 - 标准模式是:紧接
Lock()后立刻写defer Unlock(),且该函数不包含任何可能绕过它的分支逻辑 - 绝不把
defer mu.Unlock()写在 if 分支内部,除非你能保证该分支一定执行到
一个最小可验证的竞态修复示例
下面代码模拟两个 goroutine 并发累加,不加锁会输出非预期结果(如小于 200000):
package main
import (
"sync"
)
type Counter struct {
mu sync.Mutex
value int
}
func (c *Counter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}
func main() {
var wg sync.WaitGroup
c := &Counter{} // 注意:必须取地址!
for i := 0; i < 2; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 1e5; j++ {
c.Inc()
}
}()
}
wg.Wait()
println(c.value) // 总是输出 200000
}
真正容易被忽略的不是语法,而是锁的作用域是否和共享数据生命周期一致——比如在 map 中存结构体值而非指针,或在闭包中捕获了局部锁变量,都会让保护失效。










