直接用sync.Mutex即可,但必须用指针访问、成对调用Lock()/Unlock(),禁止值传递或复制,否则运行时panic;需用go vet -copylocks检查,临界区须覆盖所有共享状态读写。

直接用 sync.Mutex 就行,但必须用指针访问、成对调用 Lock()/Unlock(),否则运行时 panic 或死锁。
sync.Mutex 不能值传递,复制就 panic
Go 的 sync.Mutex 包含未导出字段,且没有拷贝检测器(直到运行时才报错)。一旦发生隐式复制,程序立即崩溃并输出:sync.Mutex is copied。
- 错误写法:
c := Counter{}; c2 := c—— 结构体赋值触发复制 - 错误写法:
func f(c Counter) { c.mu.Lock() }—— 函数参数传值 - 错误写法:
m := make(map[string]Counter); m["k"] = Counter{}—— map 赋值扩容时可能复制 - 正确姿势:始终用指针,
type Counter struct { mu sync.Mutex; val int },初始化用&Counter{},方法接收器写成func (c *Counter) Inc()
上线前务必跑 go vet -copylocks ./ 检查这类低级误用。
Lock/Unlock 必须成对,defer 不是万能的
defer mu.Unlock() 看似稳妥,但若 Lock() 后、defer 前发生 panic(比如 JSON 解析失败、DB 查询超时),Unlock() 就永远不会执行 —— 锁永远卡住,后续 goroutine 全部阻塞。
立即学习“go语言免费学习笔记(深入)”;
- 稳妥做法:把临界区逻辑封装进独立函数,确保
defer在最外层生效,例如:func (c *Counter) readAndProcess() { c.mu.Lock(); defer c.mu.Unlock(); return c.doWork() } - 避免在
Lock()和defer Unlock()之间调用不可控外部函数(如json.Unmarshal、db.QueryRow) - 不要依赖
recover来兜底释放锁 —— 逻辑复杂且易漏
读多写少时优先用 sync.RWMutex
如果共享资源以读为主(比如配置缓存、状态快照),sync.RWMutex 能显著提升吞吐量:100 个 goroutine 并发读,比 sync.Mutex 快 3–5 倍。
- 读操作用
Rlock()/RUnlock(),允许多个并发读 - 写操作仍用
Lock()/Unlock(),会阻塞所有新读写请求 - 严禁在持有读锁时尝试升级为写锁(即
RLock()后直接调Lock())—— 必死锁 - 写操作占比超过 20%,
RWMutex性能优势基本消失,此时不如回归sync.Mutex
真正难的不是写对 Lock/Unlock,而是判断“哪段代码算临界区”——它必须覆盖所有对共享状态的读写,且粒度不能太粗(否则性能差),也不能太细(否则漏保护)。实际中,先用 go run -race 跑一遍,再根据报告收缩临界区边界。


















