必须用 sync.Mutex 而非 atomic 的场景是保护结构体、map、slice 或多变量逻辑一致性;atomic 仅保证单操作原子性,无法覆盖 delete+赋值等复合操作,且错误分支易漏 unlock 导致死锁。

sync.Mutex 不是用来“学习语言”的工具,它是解决具体并发问题的机制。如果你正卡在“该不该用锁”“为什么用了还出错”“性能突然变差”这类问题上,说明你已经过了语法阶段,真正需要的是对互斥锁行为边界的实操判断。
什么时候必须用 sync.Mutex,而不是 atomic?
当你要保护的不是单个整数或指针,而是结构体字段、map、slice、或多个变量之间的逻辑一致性时,atomic 就失效了。
-
atomic只能保证单个读/写操作的原子性,比如atomic.AddInt64(&counter, 1);但它无法保护map[string]int的delete(m, k); m[k] = v这类两步操作 - 如果你在方法里先查
map再改slice,这两个操作必须包裹在同一把锁下,否则其他 goroutine 可能在中间插一脚 - 常见误判:看到“计数器”就用
atomic,结果发现还要同时更新状态标志、记录时间戳——这时就得切到sync.Mutex
Lock() 和 Unlock() 必须成对出现,但容易漏在哪?
最常漏的地方不是主流程,而是错误分支和提前返回路径。
- 函数里有多个
return,只在开头Lock(),却没在每个return前Unlock()—— 这会直接导致死锁 - 正确做法是几乎总要用
defer mu.Unlock(),但注意:defer在Lock()之后立即注册,所以必须确保Lock()成功执行(它本身不会 panic) - 别在
Unlock()后再调用Unlock(),Go 会 panic 报错:sync: unlock of unlocked mutex - 也别在没
Lock()的情况下调用Unlock(),同样 panic
TryLock() 是什么?它真能避免阻塞吗?
TryLock() 是 Go 1.18 加入的非阻塞接口,但它不解决根本竞争,只改变失败策略。
- 它返回
bool:成功拿到锁返回true,否则立刻返回false,不等待 - 适合场景:你愿意跳过这次操作(比如缓存刷新、日志采样),而不是排队等锁
- 不适合场景:必须完成的状态变更(如扣减库存、更新余额),因为
false意味着业务逻辑被丢弃 - 注意:
TryLock()成功后,仍需配对调用Unlock();失败时不持有锁,不能调用Unlock()
性能瓶颈往往不在锁本身,而在临界区代码
锁争用高 ≠ 锁设计错,更可能是临界区太重。
立即学习“go语言免费学习笔记(深入)”;
- 把耗时操作(如 HTTP 请求、JSON 解析、数据库查询)放进
Lock()/Unlock()之间,等于让所有 goroutine 排队等 IO —— 这不是锁的问题,是代码组织问题 - 典型反模式:
mu.Lock(); resp, _ := http.Get(url); data := parse(resp); cache[key] = data; mu.Unlock() - 优化方向:只锁真正共享的部分,比如
cache[key] = data这一行;HTTP 和解析放外面 - 如果临界区不可避免地长(如批量写入 map),考虑分片锁(sharded mutex)或换用
sync.RWMutex(读多写少时)
Lock() 和 Unlock() 怎么写,而是判断哪段代码必须被锁住、哪段可以挪出去、以及当 go run -race 报出竞争时,你能否一眼看出是锁粒度太粗,还是根本漏加了锁。



















