状态机核心是受控转移而非直接赋值,需类型化状态与事件、统一入口查表校验、钩子轻量异步、状态字段单独加锁。

状态机不是给状态字段赋个新值,而是用一套受控的转移规则来管理业务生命周期。核心在于把“能不能变”“变成什么”“变完做什么”三件事收口、校验、解耦。
状态与事件必须类型化,禁用字符串硬编码
用字符串字面量(如 "paid")直接写状态或事件,IDE无法提示、拼错只能运行时报错、重构时极易遗漏。正确做法是:
- 状态定义为自定义类型 + 枚举常量:例如 type State uint8,配合 const StatePaid State = iota
- 事件必须是具名 struct:如 type PayEvent struct { OrderID string; Amount int },而非 string 或 int
- 所有状态变更入口统一,例如 order.TransitionTo(event interface{}) error,禁止直接赋值 order.State = "shipped"
转移逻辑必须查表校验,不靠 if-else 散落判断
把合法转移关系显式声明为一张映射表,比如 validTransitions[StatePending][*PayEvent] = StatePaid。每次调用 TransitionTo 时先查表,不存在即返回 ErrInvalidTransition。这样:
- 新增状态或事件只需注册新规则,不改已有分支逻辑
- 非法流转(如从 canceled 直接到 shipped)在入口就被拦截
- 整张转移图可导出为文档或可视化图表,业务与开发对齐成本大幅降低
钩子只做轻量通知,重逻辑必须异步剥离
状态进入(onEnter)和退出(onExit)时触发的钩子,只负责发日志、推事件、启 goroutine,绝不执行 DB 写入、HTTP 调用、定时任务等耗时操作。例如:
- 支付成功进入 StatePaid 后,仅记录日志 + 发送 PaidEvent 到 channel
- 另起一个监听 goroutine 消费该事件,完成关超时任务、通知风控、生成发货单等后续动作
- 保证 TransitionTo 是纯内存操作,平均耗时控制在微秒级,扛住高并发写入
并发安全只锁状态本身,不锁整个业务对象
订单被支付回调、人工干预、风控拦截三路同时触发状态变更,若锁整个 Order 结构体,读金额、查用户信息等只读操作也会排队。正确方式是:
- 单独声明 stateMu sync.RWMutex 字段
- 读状态用 RLock(),写状态用 Lock()
- 其他字段(如金额、用户 ID、创建时间)保持无锁访问
- 避免因状态变更阻塞整个对象生命周期的其它读取路径










