sync.mutex是零值可用的值类型,结构体中应直接声明mu sync.mutex;若误用*sync.mutex且未初始化,调用lock()会panic;lock/unlock必须同goroutine成对出现,不可复制结构体,读多写少场景可选rwmutex。

结构体里直接声明 sync.Mutex 就能用,别用 *sync.Mutex
因为 sync.Mutex 是零值可用的值类型,内部不含指针或系统资源句柄,声明即初始化。写成 mu sync.Mutex 是正确姿势;写成 mu *sync.Mutex 却没赋值 &sync.Mutex{},调用 Lock() 会直接 panic:"invalid memory address or nil pointer dereference"。
常见翻车点:
- 从 JSON 或配置反序列化结构体时,
*sync.Mutex字段保持为nil - 用
new(MyStruct)初始化但忘了给指针字段赋值 - 把含
*sync.Mutex的结构体传给第三方库,对方未做非空检查
Lock() 和 Unlock() 必须在同一个 goroutine 里成对出现
Go 运行时强制要求:谁 Lock(),谁就得自己 Unlock()。跨 goroutine 解锁会立即 panic:"sync: unlock of unlocked mutex",不是竞态,是硬性限制。
典型错误场景:
- 在主 goroutine 调用
mu.Lock()后,起一个新 goroutine 执行mu.Unlock()(比如想实现超时释放) - 函数中间有
return或可能 panic,但没写defer mu.Unlock(),导致锁永远不释放 - 用
select+context.WithTimeout等待锁,却误以为可以在 timeout 分支里调用Unlock()
正确做法:所有 Lock() 后立刻接 defer mu.Unlock(),除非你明确需要提前释放(此时手动 Unlock() + 新的 Lock())。
结构体不能按值传递,否则 sync.Mutex 失效
sync.Mutex 不可复制,其内部含 futex 等系统级状态。一旦结构体被值拷贝(如函数参数传值、append() 切片、range 取值),副本和原结构体各自维护独立 Go 层状态,却可能指向同一内核锁资源,结果不可预测——要么 panic,要么“看似没锁住”。
具体表现:
- 函数接收者用值类型:
func (s MyStruct) Do() {}→ 方法里加锁无效 -
for _, s := range mySlice { s.Do() }→ 每次循环拿到的是副本 -
mySlice = append(mySlice, s)→ 若s含sync.Mutex,扩容时触发复制
解决方案只有一条:始终用指针接收者,结构体变量也始终以指针形式传递和存储。
读多写少时换 sync.RWMutex,但别混用 RLock() 和 Unlock()
如果临界区 80% 以上是读操作(比如查缓存 map、读配置),sync.RWMutex 能显著提升并发吞吐。它的 RLock()/RUnlock() 允许多个 reader 并发,Lock()/Unlock() 则 writer 独占。
关键陷阱:
-
RLock()后调Unlock()不报错,但锁永远不会释放(状态不匹配) - 写操作频繁或耗时长时,
RWMutex反而比sync.Mutex更慢(额外判断开销 + writer 阻塞所有 reader) - 没有“升级锁”机制:不能在持有
RLock()的前提下直接转为写锁,必须先RUnlock()再Lock()
真正容易被忽略的是:RWMutex 的性能收益高度依赖读写比例。写占比超过 20%,就该回头评估是否还值得用它。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











