不能直接 defer mu.unlock(),因为若 lock 失败或未执行就注册 defer,会导致 unlock 时 panic:“sync: unlock of unlocked mutex”;必须确保仅在成功 lock 后才注册 defer,如 defer func() { mu.unlock() }()。

为什么不能直接 defer mu.Unlock() 在锁获取失败时?
很多人写 mu.Lock() 后立刻 defer mu.Unlock(),看似简洁,但一旦锁被阻塞后因 context 超时或中断提前返回,mu.Unlock() 仍会执行——而此时线程根本没拿到锁,调用 Unlock() 会 panic:sync: unlock of unlocked mutex。
关键点在于:sync.Mutex 不是可重入的,也不记录持有者,Unlock() 必须严格对应一次成功 Lock()。所以 defer 的触发时机必须和「是否真正获得锁」强绑定。
用带状态标记的闭包 defer 最可靠
最稳妥的做法是:只在成功获取锁后,才注册一个真正安全的 defer。常见错误是把 defer 写在 Lock() 前面或无条件注册;正确方式是用匿名函数捕获锁状态:
// 正确:仅当 Lock 成功后才安排 Unlock
mu.Lock()
defer func() {
mu.Unlock()
}()
这个写法看起来多了一层函数,但它确保了:只有当前 goroutine 执行到这一行(即已成功加锁),才会把 mu.Unlock() 推入 defer 链。即使前面有 return 或 panic,也不会误解锁。
如果你需要支持带超时的获取(比如配合 context),就不能依赖原生 sync.Mutex —— 它不支持非阻塞或限时尝试。这时应换用 sync.RWMutex 配合手动轮询,或改用第三方库如 golang.org/x/sync/semaphore,但注意:这不是 Mutex 的替代,而是语义不同的同步原语。
别用 try-lock 模拟导致死锁风险
有人试图用 mu.TryLock()(实际 Go 标准库没有该方法)或通过 select + time.After 实现“伪 try-lock”,然后无条件 defer Unlock()。这是危险的:
-
sync.Mutex没有TryLock()方法,自己用select包一层无法避免竞态 —— 因为Lock()本身不是 select-case 可操作的通道操作 - 任何在未成功加锁情况下执行
Unlock()都会 panic - 用
time.After+go mu.Lock()并发抢锁,极易引发锁被多个 goroutine 误持或漏释放
结论:标准 sync.Mutex 就不支持超时或非阻塞获取。真要限时,要么用 sync.RWMutex 加读锁做轻量探测(不推荐),要么接受阻塞语义,靠上层逻辑控制调用时机(比如限制并发 goroutine 数量),或者切换到更高级的同步机制(如 errgroup.Group + context 控制生命周期)。
嵌套锁场景下 defer 更容易出错
如果函数内多次加同一把锁(例如递归调用或条件分支中重复加锁),单纯靠 defer 会释放过早或过晚。例如:
mu.Lock()
if cond {
mu.Lock() // 错!sync.Mutex 不可重入
}
defer mu.Unlock() // 这里只解一次,但可能加了两次
这种代码会 panic。Go 的 sync.Mutex 明确不支持重入,也不区分“加锁次数”。解决办法只有一个:避免嵌套加同一把锁。如果逻辑确实需要分段保护,拆成不同字段、不同 mutex,或用 sync.RWMutex 区分读写场景。
真正容易被忽略的是:defer 是按注册顺序逆序执行的,但所有 defer 都在函数 return 时统一触发 —— 所以哪怕你在中间某个分支 return,只要前面注册过 defer,它就会跑。这恰恰是优点,也是陷阱:它不看你“当前有没有锁”,只看你“有没有注册过 unlock”。所以注册动作本身就必须和锁获取成功严格耦合。











