sync.mutex 必须与共享数据同生命周期绑定,常见错误是函数内声明导致每次调用新建锁;推荐作为结构体字段配合指针接收者使用,lock/unlock需成对且覆盖所有退出路径,优先用defer,高读场景考虑sync.rwmutex或atomic。

sync.Mutex 不能靠“声明就生效”,必须和被保护的资源绑定在同一生命周期内,否则锁无效、死锁或竞态照旧。
sync.Mutex 必须和共享数据共存于同一作用域
常见错误是把 sync.Mutex 声明在函数内部或作为局部变量:
-
func bad() { mu sync.Mutex; mu.Lock(); ... }→ 每次调用都新建一把锁,完全无法跨 goroutine 同步 - 包级变量
var mu sync.Mutex可行,但仅适合保护全局变量(如var conn *net.UDPConn),粒度太粗易成瓶颈 - 最常用且安全的方式:将
sync.Mutex作为结构体字段,与数据同生命周期
例如:
type Config struct {
mu sync.Mutex
data map[string]string
}
<p>func (c *Config) Set(key, val string) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = val
}
</p>
注意:必须用指针接收者 *Config,否则值传递会复制整个结构体,c.mu 锁的是副本,原结构体不受影响。
Lock/Unlock 必须严格成对,且覆盖所有退出路径
漏掉 Unlock() 是死锁主因,尤其在有提前返回或 panic 的场景下:
-
defer mu.Unlock()很方便,但它只在Lock()执行后注册才有效;若Lock()后有return且没defer,就直接卡住 - 更稳妥写法是把临界区包进小作用域:
{ mu.Lock(); defer mu.Unlock(); /* work */ } - 禁止在锁内调用可能再次请求同一把锁的函数(Go 不支持重入锁)
-
Unlock()对未上锁的sync.Mutex会 panic,所以别乱解
读多写少时,优先考虑 sync.RWMutex
如果结构体有大量 Get() 方法、极少 Set(),用 sync.Mutex 会让所有 goroutine 串行排队,哪怕只是读:
-
RWMutex.RLock()和RWMutex.RUnlock()允许多个 goroutine 同时读 -
RWMutex.Lock()会阻塞所有新进的RLock(),直到写完成 - 但
RWMutex内部开销比sync.Mutex大,且高读负载下可能发生写饥饿(writer starvation) - 单个整数等简单类型,直接用
atomic包(如atomic.AddInt64)比锁更快、无阻塞
调试竞态不能只靠肉眼
-race 是唯一靠谱的验证手段:
-
go run -race main.go或go test -race能捕获漏锁、误锁、非同步读写 - 它不报“没调
Unlock()”,但能暴露大部分因锁失效导致的竞态 - 配合
go vet可发现部分defer位置错误、值接收者等模式问题
真正容易被忽略的,是锁的粒度和生命周期错位——比如一个结构体字段被多个互不相关的逻辑修改,却只用一把锁保护,或者锁对象本身被意外复制。这些不会立刻 crash,但会让并发行为变得不可预测。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











