图解Go状态机,并手把手带你实现和实战

680 阅读7分钟

FSM状态机的应用场景

FSM(有限状态机)广泛应用于各种系统中,尤其适用于那些具有有限状态且状态之间存在明确转换规则的场景。以下是一些典型的应用场景:

  1. 游戏开发:处理游戏中的不同状态(如菜单、游戏进行中、暂停、游戏结束等)。
  2. 用户认证系统:管理用户的登录、注销、认证等状态。
  3. 网络协议解析:处理协议的不同阶段(如握手、数据传输、断开连接等)。
  4. 设备驱动程序:管理硬件设备的不同操作模式。
  5. 工作流管理:处理复杂业务流程中的各个步骤和状态。

状态机实现

先给出状态机的架构图:

下面我们就来用 go 代码实现一个状态机。下面有代码的文字解释。

fsm.go

package api

import (
    "time"
)

// Event 是在回调中传递的事件信息。
type Event struct {
    FSM   *FSM        // FSM 是当前状态机的引用。
    Event int         // Event 是事件的名称。
    Src   int         // Src 是状态转换前的状态。
    Dst   int         // Dst 是状态转换后的状态。
    Err   error       // Err 是可选的错误信息,可以在回调中返回。
    Args  []interface{} // Args 是传递给回调函数的可选参数列表。
}

// FSM 表示有限状态机。
type FSM struct {
    current     int               // current 表示状态机的当前状态。
    transitions map[eKey]int      // transitions 映射事件和源状态到目标状态。
    callbacks   map[cKey]Callback // callbacks 映射事件和目标到回调函数。
    StateChangeTime time.Time     // StateChangeTime 记录状态改变的时间。
}

// EventDesc 定义了状态机的事件描述。
type EventDesc struct {
    Name   int      // Name 是事件名称,用于触发状态转换。
    Src    []int    // Src 是源状态的切片,定义了哪些状态可以触发该事件。
    Dst    int      // Dst 是事件触发后的目标状态。
    Before Callback // Before 是在事件触发前调用的回调函数。
    Enter  Callback // Enter 是在进入目标状态时调用的回调函数。
    After  Callback // After 是在事件触发后调用的回调函数。
}

// Callback 是回调函数的类型定义。
type Callback func(*Event)

// cKey 是用于将回调映射到目标的键结构。
type cKey struct {
    target        int // target 是目标状态。
    callbackType  int // callbackType 是回调类型(如前置、进入、后置回调)。
}

// eKey 是用于存储转换映射的键结构。
type eKey struct {
    event int // event 是事件的名称。
    src   int // src 是源状态。
}

// NewFSM 创建并初始化一个新的 FSM 实例。
func NewFSM(initial int, events []EventDesc) *FSM {
    f := &FSM{
        current:     initial,          // 初始化状态机的当前状态。
        transitions: make(map[eKey]int), // 初始化状态转换映射。
        callbacks:   make(map[cKey]Callback), // 初始化回调函数映射。
        StateChangeTime: time.Now(),  // 设置初始状态改变时间。
    }

    // 设置事件描述中的状态转换和回调函数。
    for _, e := range events {
        for _, src := range e.Src {
            f.transitions[eKey{e.Name, src}] = e.Dst
        }
        if e.Before != nil {
            f.callbacks[cKey{e.Name, callbackBeforeEvent}] = e.Before
        }
        if e.Enter != nil {
            f.callbacks[cKey{e.Name, callbackEnterState}] = e.Enter
        }
        if e.After != nil {
            f.callbacks[cKey{e.Name, callbackAfterEvent}] = e.After
        }
    }

    return f
}

// Event 触发一个事件,进行状态转换并调用相关回调函数。
func (f *FSM) Event(event int, args ...interface{}) bool {
    if f == nil {
        return false // 状态机实例为空时,返回false。
    }
    dst, ok := f.transitions[eKey{event, f.current}]
    if !ok {
        fmt.Println("未找到状态转换") // 如果找不到状态转换,打印错误信息。
        return false
    }

    e := &Event{f, event, f.current, dst, nil, args} // 创建事件对象。

    f.beforeEventCallbacks(e) // 调用事件触发前的回调函数。
    f.current = dst           // 更新状态机的当前状态。
    f.StateChangeTime = time.Now() // 更新状态改变时间。
    f.enterStateCallbacks(e)  // 调用进入状态的回调函数。
    f.afterEventCallbacks(e)  // 调用事件触发后的回调函数。

    return true
}

// beforeEventCallbacks 调用事件触发前的回调函数。
func (f *FSM) beforeEventCallbacks(e *Event) {
    if fn, ok := f.callbacks[cKey{e.Event, callbackBeforeEvent}]; ok {
        fn(e)
    }
}

// enterStateCallbacks 调用进入状态的回调函数。
func (f *FSM) enterStateCallbacks(e *Event) {
    if fn, ok := f.callbacks[cKey{e.Event, callbackEnterState}]; ok {
        fn(e)
    }
}

// afterEventCallbacks 调用事件触发后的回调函数。
func (f *FSM) afterEventCallbacks(e *Event) {
    if fn, ok := f.callbacks[cKey{e.Event, callbackAfterEvent}]; ok {
        fn(e)
    }
}

// Current 返回 FSM 当前的状态。
func (f *FSM) Current() int {
    return f.current // 返回当前状态。
}

// 回调类型常量
const (
    callbackNone int = iota
    callbackBeforeEvent
    callbackEnterState
    callbackAfterEvent
)

代码解释

这段代码实现了一个有限状态机(FSM,Finite State Machine)的框架,支持状态转换以及在转换前后执行回调函数。通过这个框架,你可以定义状态机的状态、事件和相应的回调函数,并在运行时根据事件来驱动状态的转换。以下是对这段代码的简要解释:

主要结构体和类型

  1. Event:表示在状态转换中传递的事件信息。包含了当前的状态机引用、事件名称、源状态、目标状态、错误信息以及可选的参数列表。
  2. FSM:表示有限状态机的核心结构体。包含当前状态、状态转换映射、回调函数映射以及状态改变的时间。
  3. EventDesc:定义状态机的事件描述。包含事件名称、源状态列表、目标状态以及可能的回调函数(触发前、进入状态和触发后)。
  4. Callback:定义回调函数的类型,它是一个接受 *Event 参数的函数。
  5. cKeyeKey:用于映射回调函数和状态转换的键结构,分别用于回调映射和转换映射。

主要函数

  1. NewFSM(initial int, events []EventDesc) FSM:创建并初始化一个新的状态机实例。通过传入初始状态和事件描述列表,设置状态转换和回调函数。
  2. Event(event int, args ...interface{}) bool:触发一个事件,并根据定义的状态转换执行状态的改变和相应的回调函数。首先会调用事件触发前的回调函数,然后更新当前状态和状态改变时间,接着调用进入状态的回调函数,最后调用事件触发后的回调函数。
  3. **beforeEventCallbacks(e *Event)、enterStateCallbacks(e Event)、afterEventCallbacks(e Event) :分别处理事件触发前、进入状态和事件触发后的回调函数。
  4. Current() int:返回状态机当前的状态。

回调类型常量

  • callbackNone:无回调。
  • callbackBeforeEvent:事件触发前的回调。
  • callbackEnterState:进入状态的回调。
  • callbackAfterEvent:事件触发后的回调。

使用方式

  1. 通过 NewFSM 创建一个 FSM 实例。
  2. 通过 Event 函数来触发事件,改变状态,并调用对应的回调函数。

实战代码

main.go,注意 fsm.go 在同一个目录下

package main

import (
    "fmt"
)

// 状态常量
const (
    StateIdle = iota
    StatePlaying
    StatePaused
    StateGameOver
)

// 事件常量
const (
    EventStart = iota
    EventPause
    EventResume
    EventEnd
)

// 回调函数示例
func beforeStart(e *Event) {
    fmt.Println("Before starting the game.")
}

func onEnterPlaying(e *Event) {
    fmt.Println("Entering playing state.")
}

func afterEnd(e *Event) {
    fmt.Println("After ending the game.")
}

func main() {
    // 定义事件描述
    events := []EventDesc{
        {Name: EventStart, Src: []int{StateIdle}, Dst: StatePlaying, Before: beforeStart, Enter: onEnterPlaying},
        {Name: EventPause, Src: []int{StatePlaying}, Dst: StatePaused},
        {Name: EventResume, Src: []int{StatePaused}, Dst: StatePlaying},
        {Name: EventEnd, Src: []int{StatePlaying, StatePaused}, Dst: StateGameOver, After: afterEnd},
    }

    // 创建 FSM 实例
    fsm := NewFSM(StateIdle, events)

    // 触发事件
    fmt.Println("Current state:", fsm.Current()) // 输出: Current state: 0 (StateIdle)

    fsm.Event(EventStart) // Before starting the game.  Entering playing state.

    fmt.Println("Current state:", fsm.Current()) // 输出: Current state: 1 (StatePlaying)

    fsm.Event(EventPause)
    fmt.Println("Current state:", fsm.Current()) // 输出: Current state: 2 (StatePaused)

    fsm.Event(EventResume)
    fmt.Println("Current state:", fsm.Current()) // 输出: Current state: 1 (StatePlaying)

    fsm.Event(EventEnd)                          // After ending the game.
    fmt.Println("Current state:", fsm.Current()) // 输出: Current state: 3 (StateGameOver)
}

代码结构图:

例子的详细解释

  1. 定义状态和事件常量:首先,我们定义了状态和事件的常量,以便在代码中使用。
  2. 回调函数:然后,我们定义了一些回调函数,这些函数将在状态转换前后触发。
  3. 事件描述:接下来,我们定义了事件描述,每个事件描述包含事件名称、源状态、目标状态和可选的回调函数。
  4. 创建FSM实例:使用定义的事件描述,我们创建了一个FSM实例,初始状态为 StateIdle
  5. 触发事件并检查状态:最后,我们触发了一系列事件,并在每次事件后检查当前状态,输出结果如下:
    • 开始时状态为 StateIdle
    • 触发 EventStart 后状态变为 StatePlaying
    • 触发 EventPause 后状态变为 StatePaused
    • 触发 EventResume 后状态变为 StatePlaying
    • 触发 EventEnd 后状态变为 StateGameOver

通过这个例子,可以清晰地看到FSM如何管理状态转换以及在转换过程中如何调用回调函数。这种设计使得状态管理变得更加明确和易于维护。