直接写switch-case不算真正的状态机,因其缺乏显式状态转移约束和合法性校验,无法在编译期或初始化时验证转移是否合法,易导致非法跳转引发运行时错误。

为什么直接写 switch-case 不算真正的状态机
很多人用 switch 判断 state 变量然后调用不同逻辑,这看似是状态机,但缺乏状态转移的显式约束和合法性校验。比如从 StateIdle 直接跳到 StateFailed 而不经过 StateRunning,代码里可能完全没拦住——运行时出问题才暴露。真正的状态机需要把「哪些状态能到哪些状态」提前定义清楚,最好在编译期或初始化时就校验。
用 map[State]map[Event]State 定义转移规则最实用
比手写一堆 if/switch 更可维护,也方便做合法性检查。核心结构通常是:
type State uint8
type Event uint8
var transitions = map[State]map[Event]State{
StateIdle: {
EventStart: StateRunning,
},
StateRunning: {
EventSuccess: StateDone,
EventFail: StateFailed,
},
}
使用时只需查表:next, ok := transitions[current][event]。如果 !ok,说明这个事件当前状态下不允许触发——直接返回错误或 panic,而不是静默忽略。
- 转移表建议在
init()里做完整性检查(比如每个状态是否至少有一个出边) - 避免用字符串做
State或Event,类型安全差,拼错难发现 - 如果事件携带数据,不要塞进转移表,而是让事件处理器(handler)自己解析
State 接口 + 嵌入式行为组合比继承更 Go 风格
Go 没有继承,但可以用接口 + 组合实现状态行为解耦。定义一个 State 接口:
立即学习“go语言免费学习笔记(深入)”;
type State interface {
Enter(*Context)
HandleEvent(*Context, Event) error
Exit(*Context)
}
每个具体状态(如 idleState、runningState)实现该接口,并把共享上下文(如重试次数、超时控制)放在 *Context 里。主状态机只持有 currentState State,所有行为委托过去。
- 避免在
Enter/Exit里做耗时操作;如有异步需求,改用 channel 或回调通知 - 不要让某个状态实现里直接修改
currentState字段——必须通过统一的TransitionTo()方法,确保转移日志、钩子、并发安全可控 - 测试单个状态行为时,可以 mock
*Context,不用启动整个状态机
并发场景下必须保护 currentState 和 transition 过程
多个 goroutine 同时触发事件时,currentState 可能被读写竞争。最简方案是加 sync.RWMutex,但要注意:不能只锁读/写,因为一次 HandleEvent 包含「读当前状态 → 查转移表 → 写新状态 → 执行 Enter/Exit」整套流程,必须原子化。
- 推荐把整个事件处理封装进一个带锁的方法:
func (sm *StateMachine) Handle(e Event) error - 如果性能敏感且状态转移极快,可用
atomic.Value存储State,但注意它只能存指针或不可变值,且转移逻辑仍需额外同步机制 - 别在
Enter或Exit回调里调用sm.Handle(),容易死锁或递归转移
真正难的不是定义状态和事件,而是厘清「谁负责触发转移」「失败后是否回退」「超时如何插拔进状态生命周期」——这些细节往往藏在业务边界里,而不是状态图上。










