State 接口应只定义行为方法(如 Handle(ctx, event) (State, error)),不暴露字段;具体状态用结构体实现,FSM 持有其指针并强制初始化,状态切换由 FSM 统一赋值,避免 nil panic。

State 接口怎么设计才不踩 runtime panic
Go 没有继承,State 模式靠接口 + 组合实现,但很多人一上来就定义 State 接口返回 *State 或直接嵌套指针,结果在状态切换时传了 nil 或未初始化的值,调用 Handle() 就 panic。
正确做法是让上下文(比如 FSM 结构体)持有当前 State 的指针,且所有具体状态类型都实现同一接口;初始化时强制设置初始状态,避免空指针。
-
State接口只定义行为方法(如Handle(ctx Context, event string) (State, error)),不暴露内部字段 - 每个具体状态(如
IdleState、RunningState)用结构体实现该接口,字段只存必要上下文引用(比如指向*FSM) - 状态切换必须返回新状态实例,由
FSM负责赋值,禁止在状态内部直接改fsm.currentState
如何避免状态流转逻辑散落在各个 State 实现里
把转移条件写死在每个 Handle() 方法里,会导致状态图难以维护、新增事件要改多个文件,而且没法做统一校验或日志。
更可控的方式是把转移规则抽成一张表,集中管理:事件 + 当前状态 → 目标状态 + 副作用函数。这样状态对象本身可以保持纯数据+行为,流转逻辑收口到一个地方。
立即学习“go语言免费学习笔记(深入)”;
- 用 map[[2]string]struct{ next State; effect func() } 存转移规则,key 是
[2]string{currentStateName, event} -
FSM.Handle()查表决定下一步,再调用目标状态的Enter()和旧状态的Exit()(如果需要) - 查不到规则时明确返回
ErrInvalidTransition,而不是静默忽略或 panic
FSM 的线程安全在哪几个点必须加锁
Go 里多数 FSM 实例会被多个 goroutine 并发调用(比如 HTTP handler 或消息处理协程),但状态切换和事件处理不是原子操作——常见错误是读取当前状态后,另一个 goroutine 已经改了它,导致状态错乱或重复执行副作用。
锁粒度不能太粗(比如整个 Handle 方法加 mutex),也不能太细(比如只锁状态赋值)。关键保护点就三个:
- 读取当前
state字段前,必须持有读锁(或进入临界区) - 执行状态切换(赋值
fsm.state = newState)必须在写锁下完成 - 如果
Enter()/Exit()里有共享资源操作(比如更新计数器、写日志 buffer),这部分也得包在锁里,否则日志顺序错乱或计数不准
为什么别用第三方 FSM 库(比如 go-fsm、fsm)
这些库抽象层厚,配置 DSL 复杂,调试时堆栈深、错误信息模糊(比如报 "transition not allowed" 却不说当前状态和事件是什么)。实际项目中,一个 5~8 个状态、10 几个事件的流程,手写 FSM 只需 150 行以内,可读性远高于 YAML 配置加反射调用。
真正值得复用的是状态机生命周期钩子(BeforeTransition、AfterTransition)和事件审计能力,这些自己封装一层就行,没必要引入依赖。
- 优先写明确定义的
type State interface { Handle(...) ... },比任何泛型模板都容易 debug - 打印日志时固定格式:
log.Printf("fsm: %s -> %s on %s", from, to, event),排查时直接 grep - 单元测试重点覆盖非法转移(比如从
StoppedState直接收Pause事件),而不是每个正常路径都测
状态名拼写错误、忘记实现某个 Handle 方法、并发下 Enter 和 Exit 执行顺序错位——这三类问题占了 FSM bug 的八成以上,检查清单比框架更重要。










