4. 状态机中的动作执行
任务说明
在状态机中,状态转换往往需要伴随副作用:进入新状态时发送通知、离开旧状态时清理资源、转换过程中更新业务数据。本案例以“订单状态机”为最小可复现场景,实现进入动作、退出动作和转换动作的挂载与触发机制,覆盖“待支付 → 已支付 → 已发货 → 已完成”的典型流转。
核心实现
// order_state_machine.go
package main
import (
"fmt"
"strings"
)
// 订单状态定义
type OrderState int
const (
PendingPayment OrderState = iota // 待支付
Paid // 已支付
Shipped // 已发货
Completed // 已完成
)
func (s OrderState) String() string {
switch s {
case PendingPayment:
return "待支付"
case Paid:
return "已支付"
case Shipped:
return "已发货"
case Completed:
return "已完成"
}
return "未知"
}
// 动作类型
type ActionType int
const (
OnEnter ActionType = iota // 进入动作
OnExit // 退出动作
OnTransition // 转换动作
)
// 订单结构
type Order struct {
ID string
State OrderState
Stock int // 库存余量
Notified bool // 是否已发送通知
}
// 状态机定义
type StateMachine struct {
order *Order
enterFn map[OrderState]func(*Order)
exitFn map[OrderState]func(*Order)
transFn map[[2]OrderState]func(*Order)
}
// 新建状态机并注册动作
func NewOrderStateMachine(o *Order) *StateMachine {
sm := &StateMachine{
order: o,
enterFn: make(map[OrderState]func(*Order)),
exitFn: make(map[OrderState]func(*Order)),
transFn: make(map[[2]OrderState]func(*Order)),
}
// 注册进入动作
sm.enterFn[Paid] = func(o *Order) {
o.Notified = true
fmt.Printf("[通知] 订单 %s 已支付,通知用户\n", o.ID)
}
sm.enterFn[Shipped] = func(o *Order) {
fmt.Printf("[通知] 订单 %s 已发货,通知用户\n", o.ID)
}
// 注册退出动作
sm.exitFn[PendingPayment] = func(o *Order) {
fmt.Printf("[清理] 订单 %s 离开待支付,取消未支付定时器\n", o.ID)
}
sm.exitFn[Paid] = func(o *Order) {
fmt.Printf("[清理] 订单 %s 离开已支付,关闭支付确认页\n", o.ID)
}
// 注册转换动作(更新库存)
sm.transFn[[2]OrderState{PendingPayment, Paid}] = func(o *Order) {
o.Stock -= 1
fmt.Printf("[库存] 订单 %s 支付成功,扣减库存,剩余 %d\n", o.ID, o.Stock)
}
return sm
}
// 执行状态转换
func (sm *StateMachine) TransitionTo(next OrderState) error {
current := sm.order.State
if current == next {
return fmt.Errorf("状态未变化: %s", current)
}
// 1. 执行当前状态的退出动作
if fn, ok := sm.exitFn[current]; ok {
fn(sm.order)
}
// 2. 执行转换动作
key := [2]OrderState{current, next}
if fn, ok := sm.transFn[key]; ok {
fn(sm.order)
}
// 3. 更新状态
sm.order.State = next
// 4. 执行新状态的进入动作
if fn, ok := sm.enterFn[next]; ok {
fn(sm.order)
}
return nil
}
func main() {
o := &Order{ID: "A1001", State: PendingPayment, Stock: 5}
sm := NewOrderStateMachine(o)
fmt.Printf("初始状态: %s, 库存: %d\n", o.State, o.Stock)
// 执行转换链
sm.TransitionTo(Paid)
sm.TransitionTo(Shipped)
sm.TransitionTo(Completed)
fmt.Printf("最终状态: %s, 库存: %d, 已通知: %v\n", o.State, o.Stock, o.Notified)
}
运行与验证
运行方式:
go run order_state_machine.go
预期输出(由代码逻辑唯一确定):
初始状态: 待支付, 库存: 5
[清理] 订单 A1001 离开待支付,取消未支付定时器
[库存] 订单 A1001 支付成功,扣减库存,剩余 4
[通知] 订单 A1001 已支付,通知用户
[清理] 订单 A1001 离开已支付,关闭支付确认页
[通知] 订单 A1001 已发货,通知用户
最终状态: 已完成, 库存: 4, 已通知: true
验证判定标准:
- 输出中“清理”动作发生在状态变更前,“通知”动作发生在状态变更后,顺序符合进入/退出语义;
- 库存从5减至4,仅扣减一次,证明转换动作只执行一次;
- 最终状态为“已完成”,且“已通知”为true,证明动作副作用已正确写入订单对象。
关键设计说明
- 动作注册采用三个独立map,分别挂载进入、退出和转换动作,新增状态或动作时无需修改转换逻辑;
- 转换动作以
[旧状态, 新状态]为键,支持针对特定路径的定制逻辑; - 执行顺序固定为:退出 → 转换 → 更新状态 → 进入,保证动作与状态一致性;
- 若某状态未注册动作,map查找返回false,自动跳过,不影响转换流程。