Contents

有限状态机实战:用FSM模式管理复杂业务状态流转

你的状态逻辑正在失控

订单系统。用户下单 → 待支付 → 已支付 → 待发货 → 已发货 → 已签收 → 已完成。看起来简单,但加上这些分支呢:支付超时自动取消、已发货可以退货、退款要走到单独的流程、管理员可以强制关闭。

大多数人的写法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func UpdateOrderStatus(order *Order, action string) error {
    if order.Status == "pending_payment" {
        if action == "pay" {
            order.Status = "paid"
        } else if action == "cancel" {
            order.Status = "cancelled"
        } else if action == "timeout" {
            order.Status = "cancelled"
        }
    } else if order.Status == "paid" {
        if action == "ship" {
            order.Status = "shipped"
        } else if action == "refund" {
            order.Status = "refunding"
        }
    } else if order.Status == "shipped" {
        if action == "confirm" {
            order.Status = "completed"
        } else if action == "return" {
            order.Status = "returning"
        }
    }
    // ... 还有更多分支
    return nil
}

状态多了之后,这种代码会膨胀成几百行的 if-else 瀑布。每次加新状态,你都得在这座迷宫里找到所有相关分支,小心翼翼地改。漏改一个就是 Bug。

**有限状态机(FSM)**就是解决这个问题的设计模式:把状态、事件、转移规则显式化,用一张表描述全部流转逻辑。

FSM 的三个核心概念

  • 状态(State):对象在生命周期中的某个阶段,如「待支付」「已发货」
  • 事件(Event):触发状态变化的外部动作,如「支付」「发货」「签收」
  • 转移(Transition):从某状态接收某事件后,切换到目标状态的规则

关键约束:一个状态收到一个事件,只有一个确定的目标状态(或者不允许该事件)。这正是 if-else 代码缺少的——if-else 允许同一个状态下对不同事件做任何处理,逻辑分散在各处,而FSM强制你集中声明所有规则。

手写一个FSM:转移表驱动

核心思路是用一张二维表描述「当前状态 + 事件 → 目标状态」的映射:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package fsm

// State 表示一个状态
type State string

// Event 表示一个触发事件
type Event string

// Transition 描述一条转移规则
type Transition struct {
    From     State
    Event    Event
    To       State
    Action   func(ctx interface{}) error // 可选:转移时执行的副作用
}

// Machine 状态机本体
type Machine struct {
    current     State
    transitions map[transitionKey]Transition
}

type transitionKey struct {
    from  State
    event Event
}

func New(initial State, rules []Transition) *Machine {
    m := &Machine{
        current:     initial,
        transitions: make(map[transitionKey]Transition),
    }
    for _, t := range rules {
        m.transitions[transitionKey{t.From, t.Event}] = t
    }
    return m
}

// Current 返回当前状态
func (m *Machine) Current() State { return m.current }

// Fire 触发一个事件
func (m *Machine) Fire(event Event, ctx interface{}) error {
    key := transitionKey{m.current, event}
    t, ok := m.transitions[key]
    if !ok {
        return fmt.Errorf("非法转移:状态 %s 不允许事件 %s", m.current, event)
    }
    // 执行副作用(如果有)
    if t.Action != nil {
        if err := t.Action(ctx); err != nil {
            return fmt.Errorf("转移副作用失败: %w", err)
        }
    }
    m.current = t.To
    return nil
}

// CanTransit 检查当前状态是否允许某事件
func (m *Machine) CanTransit(event Event) bool {
    _, ok := m.transitions[transitionKey{m.current, event}]
    return ok
}

用FSM重写订单状态

把分散的 if-else 变成一张声明式规则表:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
const (
    StatePendingPayment State = "pending_payment"
    StatePaid           State = "paid"
    StateShipped        State = "shipped"
    StateCompleted      State = "completed"
    StateCancelled      State = "cancelled"
    StateReturning      State = "returning"
    StateRefunded       State = "refunded"
)

const (
    EventPay     Event = "pay"
    EventShip    Event = "ship"
    EventConfirm Event = "confirm"
    EventCancel  Event = "cancel"
    EventTimeout Event = "timeout"
    EventRefund  Event = "refund"
    EventReturn  Event = "return"
)

func NewOrderFSM() *Machine {
    rules := []Transition{
        {From: StatePendingPayment, Event: EventPay, To: StatePaid,
            Action: func(ctx interface{}) error {
                o := ctx.(*Order)
                return chargePayment(o) // 实际扣款逻辑
            }},
        {From: StatePendingPayment, Event: EventCancel, To: StateCancelled},
        {From: StatePendingPayment, Event: EventTimeout, To: StateCancelled},
        {From: StatePaid, Event: EventShip, To: StateShipped},
        {From: StatePaid, Event: EventRefund, To: StateRefunded},
        {From: StateShipped, Event: EventConfirm, To: StateCompleted},
        {From: StateShipped, Event: EventReturn, To: StateReturning},
        {From: StateReturning, Event: EventRefund, To: StateRefunded},
    }
    return New(StatePendingPayment, rules)
}

// 业务调用:只需一行
func (o *Order) Pay() error {
    return o.fsm.Fire(EventPay, o)
}

func (o *Order) Ship() error {
    return o.fsm.Fire(EventShip, o)
}

对比之前的 if-else 代码:

  • 新增状态:在 rules 切片里加几行,不碰现有代码
  • 查规则:看 rules 这张表,一目了然
  • 非法操作防护:Fire 不匹配时自动报错,不会误执行
  • 副作用内聚:扣款、通知等逻辑跟转移规则绑定,不会遗漏

守卫条件:一个事件多种结果

实际场景中,同一事件在不同条件下可能转到不同状态。比如「退货」在已签收7天内允许,超过7天拒绝。加一个守卫函数:

1
2
3
4
5
6
7
type Transition struct {
    From   State
    Event  Event
    To     State
    Guard  func(ctx interface{}) bool // 守卫:返回false则跳过此规则
    Action func(ctx interface{}) error
}

Fire 方法改为按优先级遍历匹配列表,第一个通过 Guard 的规则生效:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func (m *Machine) Fire(event Event, ctx interface{}) error {
    candidates := m.transitions[transitionKey{m.current, event}]
    for _, t := range candidates {
        if t.Guard != nil && !t.Guard(ctx) {
            continue
        }
        if t.Action != nil {
            if err := t.Action(ctx); err != nil {
                return err
            }
        }
        m.current = t.To
        return nil
    }
    return fmt.Errorf("无匹配转移:状态 %s + 事件 %s", m.current, event)
}

// 注册带守卫的规则
rules := []Transition{
    {From: StateCompleted, Event: EventReturn, To: StateReturning,
        Guard: func(ctx interface{}) bool {
            o := ctx.(*Order)
            return time.Since(o.CompletedAt) < 7*24*time.Hour
        }},
}

transitions 改为 map[transitionKey][]Transition 即可支持同一 key 多条规则。

持久化:状态要落库

FSM 运行在内存里,但订单状态得存数据库。做法很简单:只持久化当前状态值,下次加载时用 New(savedState, rules) 重建状态机:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 从数据库加载
order := db.GetOrder(id)
order.fsm = New(State(order.DBStatus), rules)

// 触发事件后,把新状态写回数据库
func (o *Order) Pay() error {
    if err := o.fsm.Fire(EventPay, o); err != nil {
        return err
    }
    o.DBStatus = string(o.fsm.Current())
    return db.SaveOrder(o)
}

规则是声明式的、无状态的,每次从代码重建即可,不需要序列化。

什么时候该用FSM,什么时候别用

该用

  • 对象有明确的生命周期阶段(订单、工单、审批流、游戏角色)
  • 状态数 ≥ 4,且事件会触发状态切换
  • 非法状态转移需要被拦截

别用

  • 状态只有「开/关」两个——直接布尔值就够了
  • 状态流转极度自由(任意状态可转任意状态)——FSM 的约束反而碍事
  • 状态本身是数据驱动的连续值(如温度、进度百分比)——这不是状态机的场景

开源库推荐

不想自己造轮子的话:

  • Gogithub.com/looplab/fsm — 轻量,支持回调、守卫,社区活跃
  • Pythontransitions 库 — 功能丰富,支持嵌套状态、定时器
  • Java:Spring StateMachine — 与 Spring 生态深度集成,适合企业级流程

自造 FSM 的好处是完全可控、零依赖,核心代码不超过 100 行。如果你的状态逻辑确实复杂到需要嵌套状态、并行区域等高级特性,再引入库不迟。

小结

if-else 瀑布不是起点烂,是长大了之后不可维护。FSM 的价值不在于复杂,而在于把隐式逻辑显式化:状态、事件、转移规则集中声明,改一个分支不需要搜索整个代码库。当你发现自己在写第四层 else if 来判断同一个对象的状态时,就是引入状态机的信号。