sync.mutex 本身不能保证状态机的并发安全转换,必须配合显式状态校验;结构体需用指针接收者、mu 作匿名字段,避免值拷贝导致锁失效;transition 中须先校验再更新状态,禁止锁内耗时操作或 panic 风险。

sync.Mutex 不能直接保护状态机的并发安全转换——它只管“临界区不重叠”,不管“状态是否合法”或“转换是否被允许”。真正安全的状态机需要在加锁后做显式校验,否则锁只是把竞态变成了静默错误。
状态机字段必须用指针传递,否则 sync.Mutex 会 panic
含 sync.Mutex 的结构体一旦被值拷贝(比如传参、append() 到切片、range 取值),运行时可能触发 fatal error: sync: unlock of unlocked mutex 或锁失效。
- 错误写法:
func (s StateMachine) Transition(...) { s.mu.Lock() ... }—— 接收者是值类型,每次调用都复制整个结构体,mu字段也被复制,但底层 futex 状态没跟着复制 - 正确写法:
func (s *StateMachine) Transition(...) { s.mu.Lock() ... }—— 指针接收者,所有调用共享同一把锁 - 结构体定义时,
mu sync.Mutex应作为匿名字段;不要写成mu *sync.Mutex,零值可用且更简洁
Transition() 方法里必须校验前状态,不能只靠锁
锁能防止两个 goroutine 同时进临界区,但不能阻止非法转换,比如从 Stopped 直接跳到 Paused(中间必须经过 Running)。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 加锁后第一件事:检查当前状态是否允许本次转换,例如
if s.state != Running { return errors.New("can only pause from running") } - 转换完成后立即更新状态字段:
s.state = Paused,不要延迟到 defer 或函数末尾 - 避免在锁内做耗时操作(如 HTTP 请求、数据库查询),否则阻塞其他状态操作;必要时先解锁再处理非状态逻辑
读多写少场景下,sync.RWMutex 不适合状态机
状态机的核心操作是「写」:每次 Transition() 都要修改 state 字段。读操作(如 CurrentState())通常极少,且不值得为它引入 RWMutex 的额外开销和混用风险。
-
RLock()+Unlock()混用会静默失败,而RLock()+Unlock()组合不会 panic 却导致锁永远不释放 -
RWMutex的 writer 会阻塞所有 reader,但状态机的 reader 本就不该频繁——如果真有大量并发读状态需求,说明设计有问题:应改用事件通知或快照副本 - 简单、明确、不易出错:统一用
sync.Mutex,Lock()/Unlock()成对,defer 保底
状态变更失败时,锁必须确保释放,且不能留半截状态
状态机最怕“中间态”:锁释放了,但状态没更新成功;或者状态更新了,锁却没释放。
- 所有
return路径前必须有defer mu.Unlock()或显式mu.Unlock(),尤其注意 error 分支 - 状态赋值放在校验之后、任何可能 return 之前,例如:
mu.Lock() defer mu.Unlock() if !s.canTransition(from, to) { return ErrInvalidTransition } s.state = to // 这行不能放在 return 后面 return nil - 不要在锁内调用可能 panic 的函数(如未判空的 map 访问、索引越界),panic 会跳过 defer,导致锁泄漏
sync.Mutex,另一半靠你亲手写的那几行 if s.state == X 校验。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










