状态转移必须由显式Transition函数触发,禁止直接赋值sm.state=newState;应通过map[State]map[Event]State查表校验,调用Exit/Enter后更新状态,用指针接收器确保状态数据一致性,事件须为具名struct,转移表需可测试且不可变。

状态转移必须由显式 Transition 函数触发
直接赋值 sm.state = newState 会绕过合法性校验,导致非法跳转静默发生。正确做法是把所有转移逻辑收口到一个 Transition 方法里,它接收事件、查转移表、返回新状态或错误。
常见错误是让每个状态自己调用 ctx.SetState() —— 这会让转移逻辑分散在各处,难以统一审计和测试。应该由上下文(state machine 实例)统一调度:
-
Transition内部查sm.rules[sm.current][event],不存在则返回ErrInvalidTransition - 查到目标状态后,先调用当前状态的
Exit(),再调用目标状态的Enter() - 最后才更新
sm.current,且应加sync.RWMutex保护(若支持并发调用)
事件必须是具名 struct,不能用 string 或 int
用 "start" 或 1 表示事件,会导致拼写错误无法编译时发现、IDE 无法补全、字段扩展困难。Go 的类型系统在这里是你的第一道防线。
例如:StartEvent 和 StopEvent 是两个完全不兼容的类型,传错参数编译直接失败:
立即学习“go语言免费学习笔记(深入)”;
type StartEvent struct {
UserID string
TimeoutSec int
}
type StopEvent struct {
Reason string
}
这样设计还带来两个隐性好处:序列化时字段名明确;单元测试中可直接构造事件实例,无需字符串解析。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
状态类型必须用指针接收器实现 Handle/Enter/Exit
如果写成 func (s IdleState) Handle(e StartEvent) State,每次调用都是对副本操作,内部计数器、定时器、缓存等字段修改不会保留。下一次 Handle 调用时又回到初始值。
正确写法是 func (s *IdleState) Handle(e StartEvent) State,确保状态对象内字段变更能被后续调用感知。但要注意:
- 所有字段应为小写(非导出),仅通过方法暴露可控行为
- 若状态本身无内部状态(如纯转发型
PassThroughState),可用值接收器,但需文档注明 - 避免在
Handle中启动 goroutine 或调用阻塞 API,否则整个状态机线程卡死
转移表必须可测试、可导出、不可变
硬编码在 Transition 方法里的 if-else 不算状态机,只是带标签的条件分支。真正的转移规则应定义为 map[State]map[Event]State 类型的变量,并在初始化时一次性构建完成。
这个 map 应该是导出的(比如叫 ValidTransitions),方便外部单元测试遍历所有 (from, event) 组合断言是否得到预期 to 状态:
for from := range ValidTransitions {
for event := range ValidTransitions[from] {
to := ValidTransitions[from][event]
assert.Equal(t, expected[to], to)
}
}
容易忽略的是:map 初始化后应设为只读(可通过封装函数返回副本,或用 sync.Map 配合原子操作),防止运行时意外篡改转移逻辑。

















