logo
0
0
WeChat Login
docs(fsm): 增加说明文档

fsm 有限状态机包

提供一个基于 Go 泛型的有限状态机(Finite State Machine, FSM)实现,用于在业务流程中管理状态流转,并通过钩子(hook)在事件触发与状态切换前后执行自定义逻辑。

出于性能考虑,避免过早优化,该 FSM 为无锁实现。在并发环境下使用时,需要自行加锁。 考虑到每个 Machine 都是一个独立的状态机,一般在独立事务中调用,如果多个并发访问同时对同一实例进行操作,可以考虑交由数据库事务兜底,不必对 Machine 进行加锁。仅对 Factory 的访问加锁即可。

核心概念

  • 状态(State):由 State[S, I] 表示,S 为状态 ID(需 comparable),I 为实例类型(业务上下文)
  • 事件(Event):由 Event[E, I] 表示,E 为事件 ID(需 comparable
  • 转换(Transition):由 Ttrans[E, S] 描述 FromState + Event -> ToState
  • 工厂(Factory):由 NewFactory 构建 FSM schema,并用于创建 / 加载 Machine
  • 状态机(Machine):保存当前状态,使用 Transition 执行状态流转

NoopListener

该包提供两个默认无操作监听器:

  • NoopStateListener[I]:默认实现 StateOnEnter / OnExit
  • NoopEventListener[I]:默认实现 EventBeforeEvent / OnEvent / AfterEvent

用途:

  • 通过匿名嵌入(anonymous embedding)快速满足接口要求
  • 用户只需重写自己关心的回调方法,未重写的方法保持 no-op

使用示例

由于 FactoryMachine 仅通过泛型约束了 id 和 instance 的类型;

对于事件和状态的类型,仅要求为满足对应泛型约束的接口,并没有使用泛型约束为同一类型 ,所以可以使用 示例二 的实现方式,将每种状态和事件都实现为独立类型,省去在回调方法里对状态和事件进行判断。

作为对比,当前的实现仅约束状态和事件的类型为接口:

State[S, I]Event[E, I]

如果约束状态和事件分别必须为单一类型,对应的类型声明应为

S State[SK, I]E Event[EK, I]

此时 State 的类型不仅要求为 State 接口类型,而且必须是同一个类型 S; Event 同理。

但这里仅提供这样一种灵活性,以备特殊情况下可以兼容外部实现的类型;但出于可读性的考虑,还是建议使用第一种实现方式,将 State 和 Event 分别实现为单一类型,代码量少,可读性好。

示例一

分别为 state 和 event 定义一个类型,在回调中通过 switch 决定逻辑。

package main import ( "context" "errors" "fmt" "cnb.cool/letmelife/open/golf/workflow/fsm" ) type OrderState string type OrderEvent string const ( StateCreated OrderState = "created" StatePaid OrderState = "paid" StateCanceled OrderState = "canceled" ) const ( EventPay OrderEvent = "pay" EventCancel OrderEvent = "cancel" ) type Order struct { ID int64 } type orderState struct { fsm.NoopStateListener[*Order] id OrderState } func (s orderState) ID() OrderState { return s.id } func (s orderState) OnEnter(ctx context.Context, o *Order) error { switch s.id { case StatePaid: fmt.Printf("order=%d enter paid\n", o.ID) case StateCanceled: fmt.Printf("order=%d enter canceled\n", o.ID) } return nil } type orderEvent struct { fsm.NoopEventListener[*Order] id OrderEvent } func (e orderEvent) ID() OrderEvent { return e.id } func (e orderEvent) BeforeEvent(ctx context.Context, o *Order) error { switch e.id { case EventCancel: if o.ID <= 0 { return errors.New("invalid order id") } } return nil } func (e orderEvent) OnEvent(ctx context.Context, o *Order) error { switch e.id { case EventPay: fmt.Printf("pay order=%d\n", o.ID) case EventCancel: fmt.Printf("cancel order=%d\n", o.ID) } return nil } func main() { // 仅作示例,实际应用中需要使用上层传入的真实 context,避免使用默认的 background context ctx := context.Background() created := orderState{id: StateCreated} paid := orderState{id: StatePaid} canceled := orderState{id: StateCanceled} pay := orderEvent{id: EventPay} cancel := orderEvent{id: EventCancel} factory, err := fsm.NewFactory[OrderState, OrderEvent, *Order]( StateCreated, []fsm.State[OrderState, *Order]{created, paid, canceled}, []fsm.Event[OrderEvent, *Order]{pay, cancel}, []fsm.Ttrans[OrderEvent, OrderState]{ {Event: EventPay, FromState: StateCreated, ToState: StatePaid}, {Event: EventCancel, FromState: StateCreated, ToState: StateCanceled}, {Event: EventCancel, FromState: StatePaid, ToState: StateCanceled}, }, nil, ) if err != nil { panic(err) } m, err := factory.Create(&Order{ID: 1}) if err != nil { panic(err) } _ = m.Transition(ctx, EventPay) err = m.Transition(ctx, EventPay) if errors.Is(err, fsm.ErrInvalidTransition) { fmt.Printf("invalid transition: state=%s event=%s\n", m.Current(), EventPay) } }

示例二

state / event 分别由多个类型实现。

package main import ( "context" "errors" "fmt" "cnb.cool/letmelife/open/golf/workflow/fsm" ) type OrderState string type OrderEvent string const ( StateCreated OrderState = "created" StatePaid OrderState = "paid" StateCanceled OrderState = "canceled" ) const ( EventPay OrderEvent = "pay" EventCancel OrderEvent = "cancel" ) type Order struct { ID int64 } type createdState struct{ fsm.NoopStateListener[*Order] } func (createdState) ID() OrderState { return StateCreated } type paidState struct{ fsm.NoopStateListener[*Order] } func (paidState) ID() OrderState { return StatePaid } type canceledState struct{ fsm.NoopStateListener[*Order] } func (canceledState) ID() OrderState { return StateCanceled } type payEvent struct{ fsm.NoopEventListener[*Order] } func (payEvent) ID() OrderEvent { return EventPay } func (payEvent) OnEvent(ctx context.Context, o *Order) error { fmt.Printf("pay order=%d\n", o.ID) return nil } type cancelEvent struct{ fsm.NoopEventListener[*Order] } func (cancelEvent) ID() OrderEvent { return EventCancel } func (cancelEvent) BeforeEvent(ctx context.Context, o *Order) error { if o.ID <= 0 { return errors.New("invalid order id") } return nil } func main() { // 仅作示例,实际应用中需要使用上层传入的真实 context,避免使用默认的 background context ctx := context.Background() factory, err := fsm.NewFactory[OrderState, OrderEvent, *Order]( StateCreated, []fsm.State[OrderState, *Order]{createdState{}, paidState{}, canceledState{}}, []fsm.Event[OrderEvent, *Order]{payEvent{}, cancelEvent{}}, []fsm.Ttrans[OrderEvent, OrderState]{ {Event: EventPay, FromState: StateCreated, ToState: StatePaid}, {Event: EventCancel, FromState: StateCreated, ToState: StateCanceled}, {Event: EventCancel, FromState: StatePaid, ToState: StateCanceled}, }, nil, ) if err != nil { panic(err) } m, err := factory.Create(&Order{ID: 1}) if err != nil { panic(err) } _ = m.Transition(ctx, EventPay) err = m.Transition(ctx, EventPay) if errors.Is(err, fsm.ErrInvalidTransition) { fmt.Printf("invalid transition: state=%s event=%s\n", m.Current(), EventPay) } }

Hook 执行顺序与状态更新语义

一次 Transition(ctx, event) 的执行顺序如下:

  1. Event.BeforeEvent
  2. State.OnExit(当前状态)
  3. Event.OnEvent
  4. State.OnEnter(目标状态)
  5. StateSaver.Save(如果配置了 persister)
  6. Event.AfterEvent

状态更新语义:

  • BeforeEvent / OnExit / OnEvent / OnEnter / Save 任一阶段失败:状态保持不变(仍为 FromState)
  • AfterEvent 失败:状态已切换到 ToState,但会返回错误

状态持久化(可选)

通过 StatePersister[I, S] 注入 Save/Load 逻辑:

  • Machine.TransitionOnEnter 成功后调用 Save(ctx, instance, nextState)
  • Factory.Load(ctx, instance) 通过 Load 获取实例当前状态并构建 Machine
type memPersister struct { store map[int64]OrderState } func (p *memPersister) Save(ctx context.Context, o *Order, s OrderState) error { if p.store == nil { p.store = make(map[int64]OrderState) } p.store[o.ID] = s return nil } func (p *memPersister) Load(ctx context.Context, o *Order) (OrderState, error) { return p.store[o.ID], nil }

常见错误

该包提供了一组可用于 errors.Is 判断的 sentinel error:

  • ErrInvalidTransition:当前状态下不允许该事件
  • ErrInvalidInitState / ErrInvalidState / ErrInvalidEvent:schema 构建或加载时校验失败
  • ErrDuplicateState / ErrDuplicateEvent / ErrDuplicateTransition:schema 定义冲突
  • ErrEventsNotCovered:定义的事件未被 transitions 覆盖(每个 Event 至少出现一次)
  • ErrHookFailed:钩子执行失败
  • ErrStateLoadFailed / ErrStateSaveFailed:状态加载 / 保存失败
  • ErrStateLoaderNotAvailable:未提供 loader,但调用了 Factory.Load
  • ErrDeprecated:schema 已被废弃(见下)

错误通常会携带额外上下文信息(如 state、event、hook、instance),便于定位问题。

Schema 废弃

当 schema 发生变更且希望禁用旧状态机时,可调用 Factory.Deprecate()

  • Factory.Create / Factory.Load 返回 ErrDeprecated
  • 由该 Factory 创建的 Machine.Transition 也会返回 ErrDeprecated

一般用于状态机定义发生变更时,用于废弃旧的状态机 factory,并构建新 factory 代替。