Go 語言 fsm 源碼解讀,這一次讓你徹底學會有限狀態機

我在文章《在 Go 中如何使用有限狀態機優雅解決程序中狀態轉換問題》中講解了有限狀態機的概念,並介紹了 Go 中有限狀態機 fsm 包的使用。本篇文章,我將更進一步,直接通過解讀源碼的方式,讓你深刻理解 fsm 是如何實現的,這一次你將徹底掌握有限狀態機。

源碼解讀

廢話不多說,我們直接上代碼。

結構體

首先 fsm 包定義了一個結構體 FSM 用來表示狀態機。

https://github.com/looplab/fsm/blob/main/fsm.go#L40

// FSM 是持有「當前狀態」的狀態機。
type FSM struct {
    // FSM 當前狀態
    current string

    // transitions 將「事件和原狀態」映射到「目標狀態」。
    transitions map[eKey]string

    // callbacks 將「回調類型和目標」映射到「回調函數」。
    callbacks map[cKey]Callback

    // transition 是內部狀態轉換函數,可以直接使用,也可以在異步狀態轉換時調用。
    transition func()
    // transitionerObj 用於調用 FSM 的 transition() 函數。
    transitionerObj transitioner

    // stateMu 保護對當前狀態的訪問。
    stateMu sync.RWMutex
    // eventMu 保護對 Event() 和 Transition() 兩個函數的調用。
    eventMu sync.Mutex

    // metadata 可以用來存儲和加載可能跨事件使用的數據
    // 使用 SetMetadata() 和 Metadata() 方法來存儲和加載數據。
    metadata map[string]interface{}
    // metadataMu 保護對元數據的訪問。
    metadataMu sync.RWMutex
}

我們知道,有限狀態機中最重要的三個特徵如下:

所以,<font>FSM</font> 結構體中一定包含與這些特徵有關的字段。

current 表示狀態機的當前狀態。

transitions 用於記錄狀態轉換規則,即定義觸發某一事件時,允許從某一種狀態,轉換成另一種狀態。它是一個 map 對象,其 key 爲 eKey 類型:

// eKey is a struct key used for storing the transition map.
type eKey struct {
    // event is the name of the event that the keys refers to.
    event string

    // src is the source from where the event can transition.
    src string
}

eKey 類型用來記錄事件和原狀態。map 的 value 爲 string 類型,用來記錄目標狀態。

callbacks 用於記錄事件觸發時的回調函數。它也是一個 map 對象,其 key 爲 cKey 類型:

// cKey is a struct key used for keeping the callbacks mapped to a target.
type cKey struct {
    // target is either the name of a state or an event depending on which
    // callback type the key refers to. It can also be "" for a non-targeted
    // callback like before_event.
    target string

    // callbackType is the situation when the callback will be run.
    callbackType int
}

cKey 類型用來記錄目標和回調類型,其中目標可以是狀態事件名稱,回調類型可選值如下:

const (
    // 未設置回調
    callbackNone int = iota
    // 事件觸發前執行的回調
    callbackBeforeEvent
    // 離開舊狀態前執行的回調
    callbackLeaveState
    // 進入新狀態是執行的回調
    callbackEnterState
    // 事件完成時執行的回調
    callbackAfterEvent
)

回調類型決定了回調函數的執行時機。

map 的 value 爲回調函數,其聲明類型如下:

// Callback is a function type that callbacks should use. Event is the current
// event info as the callback happens.
type Callback func(context.Context, *Event)

還記得回調函數是如何註冊的嗎?

fsm.Callbacks{
    // 任一事件發生之前觸發
    "before_event": func(_ context.Context, e *fsm.Event) {
        color.HiMagenta("| before event\t | %s | %s |", e.Src, e.Dst)
    },
}

這裏註冊的 before_event 回調函數簽名就是 Callback 類型。

當然這裏還使用了 fsm.Callbacks 類型來註冊,想必你已經猜到了 fsm.Callbacks 的類型:

// Callbacks is a shorthand for defining the callbacks in NewFSM.
type Callbacks map[string]Callback

接下來的 transition 和 transitionerObj 兩個屬性是用來實現狀態轉換的,暫且留到後續使用時再來研究。

這裏還有兩個互斥鎖,分別用來保護對當前狀態的訪問(stateMu),和保證事件觸發時的操作併發安全(eventMu)。

最後 FSM 還提供了 metadata 和 metadataMu 兩個屬性,這倆屬性用於管理元數據信息,後文中我會演示其使用場景。

現在,我們可以總結一下 FSM 結構體定義:

FSM

接下來,我將對 FSM 結構體所實現的方法進行講解。

方法

我們先來看一下 FSM 結構體都提供了哪些方法和能力:

FSM

這裏列出了 FSM 結構體實現的所有方法,並且做了分類,你先有個感官上的認識,接下來我們依次解讀。

構造函數

我們最先要分析的源碼,當然是 FSM 結構體的構造函數了,其實現如下:

func NewFSM(initial string, events []EventDesc, callbacks map[string]Callback) *FSM {
    // 構造有限狀態機 FSM
    f := &FSM{
        transitionerObj: &transitionerStruct{},        // 狀態轉換器,使用默認實現
        current:         initial,                      // 當前狀態
        transitions:     make(map[eKey]string),        // 存儲「事件和原狀態」到「目標狀態」的轉換規則映射
        callbacks:       make(map[cKey]Callback),      // 回調函數映射表
        metadata:        make(map[string]interface{}), // 元信息
    }

    // 構建 f.transitions map,並且存儲所有的「事件」和「狀態」集合
    allEvents := make(map[string]bool) // 存儲所有事件的集合
    allStates := make(map[string]bool) // 存儲所有狀態的集合
    for _, e := range events {         // 遍歷事件列表,提取並存儲所有事件和狀態
        for _, src := range e.Src {
            f.transitions[eKey{e.Name, src}] = e.Dst
            allStates[src] = true
            allStates[e.Dst] = true
        }
        allEvents[e.Name] = true
    }

    // 提取「回調函數」到「事件和原狀態」的映射關係,並註冊到 callbacks
    for name, fn := range callbacks {
        var target string    // 目標:狀態/事件
        var callbackType int// 回調類型(決定了調用順序)

        // 根據回調函數名稱前綴分類
        switch {
        // 事件觸發前執行
        case strings.HasPrefix(name, "before_"):
            target = strings.TrimPrefix(name, "before_")
            if target == "event" { // 全局事件前置鉤子(任何事件觸發都會調用,如用於日誌記錄場景)
                target = ""// 將 target 置空
                callbackType = callbackBeforeEvent
            } elseif _, ok := allEvents[target]; ok { // 在特定事件前執行
                callbackType = callbackBeforeEvent
            }
        // 離開當前狀態前執行
        case strings.HasPrefix(name, "leave_"):
            target = strings.TrimPrefix(name, "leave_")
            if target == "state" { // 全局狀態離開鉤子
                target = ""
                callbackType = callbackLeaveState
            } elseif _, ok := allStates[target]; ok { // 離開舊狀態前執行
                callbackType = callbackLeaveState
            }
        // 進入新狀態後執行
        case strings.HasPrefix(name, "enter_"):
            target = strings.TrimPrefix(name, "enter_")
            if target == "state" { // 全局狀態進入鉤子
                target = ""
                callbackType = callbackEnterState
            } elseif _, ok := allStates[target]; ok { // 進入新狀態後執行
                callbackType = callbackEnterState
            }
        // 事件完成後執行
        case strings.HasPrefix(name, "after_"):
            target = strings.TrimPrefix(name, "after_")
            if target == "event" { // 全局事件後置鉤子
                target = ""
                callbackType = callbackAfterEvent
            } elseif _, ok := allEvents[target]; ok { // 事件完成後執行
                callbackType = callbackAfterEvent
            }
        // 處理未加前綴的回調(簡短版本)
        default:
            target = name                       // 狀態/事件
            if _, ok := allStates[target]; ok { // 如果 target 爲某個狀態,則 callbackType 會置爲與 enter_[target] 相同
                callbackType = callbackEnterState
            } elseif _, ok := allEvents[target]; ok { // 如果 target 爲某個事件,則 callbackType 會置爲與 after_[target] 相同
                callbackType = callbackAfterEvent
            }
        }

        // 記錄 callbacks map
        if callbackType != callbackNone {
            // key: callbackType(用於決定執行順序) + target(如果是全局鉤子,則 target 爲空,否則,target 爲狀態/事件)
            // val: 事件觸發時需要執行的回調函數
            f.callbacks[cKey{target, callbackType}] = fn
        }
    }

    return f
}

構造函數內部代碼比較多,我們可以將它的核心邏輯分爲 3 塊,分別是:構造有限狀態機 FSM、記錄事件(event)和狀態(state)、註冊回調函數。

構造有限狀態機 FSM 部分的代碼比較簡單:

// 構造有限狀態機 FSM
f := &FSM{
    transitionerObj: &transitionerStruct{},        // 狀態轉換器,使用默認實現
    current:         initial,                      // 當前狀態
    transitions:     make(map[eKey]string),        // 存儲「事件和原狀態」到「目標狀態」的轉換規則映射
    callbacks:       make(map[cKey]Callback),      // 回調函數映射表
    metadata:        make(map[string]interface{}), // 元信息
}

使用函數參數 initial 作爲狀態機的當前狀態,幾個 map 類型的屬性,都賦予了默認值。

接下來的部分代碼邏輯用於記錄事件(event)和狀態(state):

// 構建 f.transitions map,並且存儲所有的「事件」和「狀態」集合
allEvents := make(map[string]bool) // 存儲所有事件的集合
allStates := make(map[string]bool) // 存儲所有狀態的集合
for _, e := range events {         // 遍歷事件列表,提取並存儲所有事件和狀態
    for _, src := range e.Src {
        f.transitions[eKey{e.Name, src}] = e.Dst
        allStates[src] = true
        allStates[e.Dst] = true
    }
    allEvents[e.Name] = true
}

這裏 allEvents 和 allStates 都是集合類型(Set),分別用於記錄所有註冊的事件和狀態。

最後這一部分代碼用來註冊回調函數:

for name, fn := range callbacks {
    var target string    // 目標:狀態/事件
    var callbackType int// 回調類型(決定了調用順序)

    // 根據回調函數名稱前綴分類
    switch {
    // 事件觸發前執行
    case strings.HasPrefix(name, "before_"):
        target = strings.TrimPrefix(name, "before_")
        if target == "event" { // 全局事件前置鉤子(任何事件觸發都會調用,如用於日誌記錄場景)
            target = ""// 將 target 置空
            callbackType = callbackBeforeEvent
        } elseif _, ok := allEvents[target]; ok { // 在特定事件前執行
            callbackType = callbackBeforeEvent
        }
    ...
    }

    // 記錄 callbacks map
    if callbackType != callbackNone {
        // key: callbackType(用於決定執行順序) + target(如果是全局鉤子,則 target 爲空,否則,target 爲狀態/事件)
        // val: 事件觸發時需要執行的回調函數
        f.callbacks[cKey{target, callbackType}] = fn
    }
}

這裏遍歷了 callbacks 列表,並根據回調函數名稱前綴分類,然後註冊到 f.callbacks 屬性的 map 對象中。

NOTE:

代碼註釋中的 “鉤子” 就代表回調函數,只不過是另一種叫法罷了。

我們再來回顧一下回調函數是如何註冊的:

fsm.Callbacks{
    "before_event": func(_ context.Context, e *fsm.Event) { ... },
}

這個參數被傳入構造函數後,會進入 strings.HasPrefix(name, "before_")這個 case,然後if target == "event"成立,此時target將會被置空,回調類型callbackType將被賦值爲callbackBeforeEvent。如果我們註冊的是before_closed回調函數,則target值爲closed。對於target不同處理,將決定最後回調函數的執行順序。我們暫且不繼續深入,留個懸念,後續解讀回調函數相關的源碼,你就能白爲什麼了。

不過,我還要特別強調一下 default 分支的 case:

default:
    target = name                       // 狀態/事件
    if _, ok := allStates[target]; ok { // 如果 target 爲某個狀態,則 callbackType 會置爲與 enter_[target] 相同,即二者等價
        callbackType = callbackEnterState
    } else if _, ok := allEvents[target]; ok { // 如果 target 爲某個事件,則 callbackType 會置爲與 after_[target] 相同,即二者等價
        callbackType = callbackAfterEvent
    }
}

還記得在上一篇文章中我提到過,註冊 closed 事件等價於 enter_closed 事件嗎?就是在 default 這個 case 中實現的。

FSM Event

對於構造函數的講解就到這裏,裏面一些具體的代碼細節你可能現在有點發懵,沒關係,接着往下看,你的疑惑都將被解開。

當前狀態

接着,我們來看一下與當前狀態相關的這幾個方法源碼是如何實現的,它們的代碼其實都很簡單,我就不一一解讀了,我把源碼貼在這裏,你一看就能明白:

// Current 返回 FSM 的當前狀態。
func (f *FSM) Current() string {
    f.stateMu.RLock()
    defer f.stateMu.RUnlock()
    return f.current
}

// Is 判斷 FSM 當前狀態是否爲指定狀態。
func (f *FSM) Is(state string) bool {
    f.stateMu.RLock()
    defer f.stateMu.RUnlock()
    return state == f.current
}

// SetState 將 FSM 從當前狀態轉移到指定狀態。
// 此調用不觸發任何回調函數(如果定義)。
func (f *FSM) SetState(state string) {
    f.stateMu.Lock()
    defer f.stateMu.Unlock()
    f.current = state
}

// Can 判斷 FSM 在當前狀態下,是否可以觸發指定事件,如果可以,則返回 true。
func (f *FSM) Can(event string) bool {
    f.eventMu.Lock()
    defer f.eventMu.Unlock()
    f.stateMu.RLock()
    defer f.stateMu.RUnlock()
    _, ok := f.transitions[eKey{event, f.current}]
    return ok && (f.transition == nil)
}

func (f *FSM) Cannot(event string) bool {
    return !f.Can(event)
}

// AvailableTransitions 返回當前狀態下可用的轉換列表。
func (f *FSM) AvailableTransitions() []string {
    f.stateMu.RLock()
    defer f.stateMu.RUnlock()
    var transitions []string
    for key := range f.transitions {
        if key.src == f.current {
            transitions = append(transitions, key.event)
        }
    }
    return transitions
}
狀態轉換

與狀態轉換相關的方法可以說是 FSM 最重要的方法了。

我們先來看 Event 方法的實現:

// Event 通過指定事件名稱觸發狀態轉換
func (f *FSM) Event(ctx context.Context, event string, args ...interface{}) error {
    f.eventMu.Lock() // 事件互斥鎖鎖定

    // 爲了始終解鎖事件互斥鎖(eventMu),此處添加了 defer 防止狀態轉換完成後執行 enter/after 回調時仍持有鎖;
    // 因爲這些回調可能觸發新的狀態轉換,故在下方代碼中需要顯式解鎖
    var unlocked bool// 標記是否已經解鎖
    deferfunc() {
        if !unlocked { // 如果下方的邏輯已經顯式操作過解鎖,defer 中無需重複解鎖
            f.eventMu.Unlock()
        }
    }()

    f.stateMu.RLock() // 獲取狀態讀鎖
    defer f.stateMu.RUnlock()

    // NOTE: 之前的轉換尚未完成
    if f.transition != nil {
        // 上一次狀態轉換還未完成,返回"前一個轉換未完成"錯誤
        return InTransitionError{event}
    }

    // NOTE: 事件 event 在當前狀態 current 下是否適用,即是否在 transitions 表中
    dst, ok := f.transitions[eKey{event, f.current}]
    if !ok { // 無效事件
        for ekey := range f.transitions {
            if ekey.event == event {
                // 事件和當前狀態不對應
                return InvalidEventError{event, f.current}
            }
        }
        // 未定義的事件
        return UnknownEventError{event}
    }

    ctx, cancel := context.WithCancel(ctx)
    defer cancel()
    // 構造一個事件對象
    e := &Event{f, event, f.current, dst, nil, args, false, false, cancel}

    // NOTE: 執行 before 鉤子
    err := f.beforeEventCallbacks(ctx, e)
    if err != nil {
        return err
    }

    // NOTE: 當前狀態等於目標狀態,無需轉換
    if f.current == dst {
        f.stateMu.RUnlock()
        defer f.stateMu.RLock()
        f.eventMu.Unlock()
        unlocked = true
        // NOTE: 執行 after 鉤子
        f.afterEventCallbacks(ctx, e)
        return NoTransitionError{e.Err}
    }

    // 定義狀態轉換閉包函數
    transitionFunc := func(ctx context.Context, async bool)func() {
        returnfunc() {
            if ctx.Err() != nil {
                if e.Err == nil {
                    e.Err = ctx.Err()
                }
                return
            }

            f.stateMu.Lock()
            f.current = dst    // 狀態轉換
            f.transition = nil// NOTE: 標記狀態轉換完成
            f.stateMu.Unlock()

            // 顯式解鎖 eventMu 事件互斥鎖,允許 enterStateCallbacks 回調函數觸發新的狀態轉換操作(避免死鎖)
            // 對於異步狀態轉換,無需顯式解鎖,鎖已在觸發異步操作時釋放
            if !async {
                f.eventMu.Unlock()
                unlocked = true
            }
            // NOTE: 執行 enter 鉤子
            f.enterStateCallbacks(ctx, e)
            // NOTE: 執行 after 鉤子
            f.afterEventCallbacks(ctx, e)
        }
    }

    // 記錄狀態轉換函數(這裏標記爲同步轉換)
    f.transition = transitionFunc(ctx, false)

    // NOTE: 執行 leave 鉤子
    if err = f.leaveStateCallbacks(ctx, e); err != nil {
        if _, ok := err.(CanceledError); ok {
            f.transition = nil// NOTE: 如果通過 ctx 取消了,則標記爲 nil,無需轉換
        } elseif asyncError, ok := err.(AsyncError); ok { // NOTE: 如果是 AsyncError,說明是異步轉換
            // 爲異步操作創建獨立上下文,以便異步狀態轉換正常工作
            // 這個新的 ctx 實際上已經脫離了原始 ctx,原 ctx 取消不會影響當前 ctx
            // 不過新的 ctx 保留了原始 ctx 的值,所有通過 ctx 傳遞的值還可以繼續使用
            ctx, cancel := uncancelContext(ctx)
            e.cancelFunc = cancel                    // 綁定新取消函數
            asyncError.Ctx = ctx                     // 傳遞新上下文
            asyncError.CancelTransition = cancel     // 暴露取消接口
            f.transition = transitionFunc(ctx, true) // NOTE: 標記爲異步轉換狀態
            // NOTE: 如果是異步轉換,直接返回,不會同步調用 f.doTransition(),需要用戶手動調用 f.Transition() 來觸發狀態轉換
            return asyncError
        }
        return err
    }

    // Perform the rest of the transition, if not asynchronous.
    f.stateMu.RUnlock()
    defer f.stateMu.RLock()
    err = f.doTransition() // NOTE: 執行狀態轉換邏輯,即調用 f.transition()
    if err != nil {
        return InternalError{}
    }

    return e.Err
}

因爲 Event 是核心方法,所以源碼會比較多,我們一起來梳理下核心邏輯。

首先,Event 方法會判斷上一次的狀態轉換是否完成:

// NOTE: 之前的轉換尚未完成
if f.transition != nil {
    // 上一次狀態轉換還未完成,返回"前一個轉換未完成"錯誤
    return InTransitionError{event}
}

是否轉換完成的標誌是 f.transition 是否爲 nil,如果上一次狀態轉換尚未完成,則返回一個 Sentinel Error。

接着,需要判斷當前觸發的事件是否有效:

// NOTE: 事件 event 在當前狀態 current 下是否適用,即是否在 transitions 表中
dst, ok := f.transitions[eKey{event, f.current}]
if !ok { // 無效事件
    for ekey := range f.transitions {
        if ekey.event == event {
            // 事件和當前狀態不對應
            return InvalidEventError{event, f.current}
        }
    }
    // 未定義的事件
    return UnknownEventError{event}
}

前文中我們說過 f.transitions 用於記錄狀態轉換規則,即定義觸發某一事件時,允許從某一種狀態,轉換成另一種狀態。

如果在 f.transitions 表中查不到任何一條與當前狀態和事件對應的數據,則表示無效事件,同樣會返回指定的 Sentinel Error。

這些檢查都通過後,就會構造一個事件對象:

ctx, cancel := context.WithCancel(ctx)
defer cancel()
// 構造一個事件對象
e := &Event{f, event, f.current, dst, nil, args, false, false, cancel}

接下來,就到了狀態轉換的核心邏輯了。而所有的回調函數,也是在這個時候開始觸發執行的。

在執行狀態轉換之前,首先要執行的就是 before 類回調函數:

// NOTE: 執行 before 鉤子
err := f.beforeEventCallbacks(ctx, e)
if err != nil {
    return err
}

執行完 before 類回調函數,會再對狀態做一次檢查:

// NOTE: 當前狀態等於目標狀態,無需轉換
if f.current == dst {
    f.stateMu.RUnlock()
    defer f.stateMu.RLock()
    f.eventMu.Unlock()
    unlocked = true
    // NOTE: 執行 after 鉤子
    f.afterEventCallbacks(ctx, e)
    return NoTransitionError{e.Err}
}

如果狀態機的當前狀態等於目標狀態,則無需狀態轉換,那麼直接執行 after 類回調函數就行了,最終返回指定的 Sentinel Error。

否則,需要進行狀態轉換。此時,狀態轉換也不會直接進行,而是會定義一個狀態轉換閉包函數並賦值給 f.transition

    // 定義狀態轉換閉包函數
    transitionFunc := func(ctx context.Context, async bool) func() {
        return func() {
            ...
        }
    }

    // 記錄狀態轉換函數(這裏標記爲同步轉換)
    f.transition = transitionFunc(ctx, false)

狀態轉換函數第二個參數用來標記同步轉換還是異步轉換,這裏標記爲同步轉換。對於異步轉換邏輯,我們後面再來講解。

接下來會先執行 leave 類的回調函數:

// NOTE: 執行 leave 鉤子
if err = f.leaveStateCallbacks(ctx, e); err != nil {
    ...
}

這是調用的第二個回調函數。

最後,終於到了執行狀態轉換的邏輯了:

err = f.doTransition() // NOTE: 執行狀態轉換邏輯,即調用 f.transition()
if err != nil {
    return InternalError{}
}

這裏調用了 f.doTransition() 函數,其定義如下:

// doTransition wraps transitioner.transition.
func (f *FSM) doTransition() error {
    return f.transitionerObj.transition(f)
}

可以發現,其內部正式調用了 f.transitionerObj 屬性的 transition 方法。

還記得 f.transitionerObj 屬性是何時賦值嗎?在 NewFSM 構造函數中,其賦值如下:

// 構造有限狀態機 FSM
f := &FSM{
    transitionerObj: &transitionerStruct{},        // 狀態轉換器,使用默認實現
    current:         initial,                      // 當前狀態
    transitions:     make(map[eKey]string),        // 存儲「事件和原狀態」到「目標狀態」的轉換規則映射
    callbacks:       make(map[cKey]Callback),      // 回調函數映射表
    metadata:        make(map[string]interface{}), // 元信息
}

所以我們需要看一下 transitionerStruct 的具體實現:

// transitioner 是 FSM 的狀態轉換函數接口。
type transitioner interface {
    transition(*FSM) error
}

// 狀態轉換接口的默認實現
type transitionerStruct struct{}

// Transition completes an asynchronous state change.
//
// The callback for leave_<STATE> must previously have called Async on its
// event to have initiated an asynchronous state transition.
func (t transitionerStruct) transition(f *FSM) error {
    if f.transition == nil {
        return NotInTransitionError{}
    }
    f.transition()
    returnnil
}

f.transitionerObj 屬性聲明的是 transitioner 接口類型,而 transitionerStruct 結構體則是這個接口的默認實現。

transitionerStruct.transition 方法內部最終還是在調用 f.transition() 方法。

而 f.transition 方法,也就是前文中定義的那個閉包函數:

// 定義狀態轉換閉包函數
transitionFunc := func(ctx context.Context, async bool)func() {
    returnfunc() {
        if ctx.Err() != nil {
            if e.Err == nil {
                e.Err = ctx.Err()
            }
            return
        }

        f.stateMu.Lock()
        f.current = dst    // 狀態轉換
        f.transition = nil// NOTE: 標記狀態轉換完成
        f.stateMu.Unlock()

        // 顯式解鎖 eventMu 事件互斥鎖,允許 enterStateCallbacks 回調函數觸發新的狀態轉換操作(避免死鎖)
        // 對於異步狀態轉換,無需顯式解鎖,鎖已在觸發異步操作時釋放
        if !async {
            f.eventMu.Unlock()
            unlocked = true
        }
        // NOTE: 執行 enter 鉤子
        f.enterStateCallbacks(ctx, e)
        // NOTE: 執行 after 鉤子
        f.afterEventCallbacks(ctx, e)
    }
}

// 記錄狀態轉換函數(這裏標記爲同步轉換)
f.transition = transitionFunc(ctx, false)

閉包函數的 async 參數用來標記同步或異步,我們暫且不關心異步,這裏只關注同步邏輯。

其實,這裏的核心邏輯就是完成狀態轉換:

f.current = dst    // 狀態轉換
f.transition = nil // NOTE: 標記狀態轉換完成

狀態轉換完成後,將 f.transition 標記爲 nil。所以根據這個屬性的值,就能判斷上一次狀態轉換是否完成。

狀態轉換完成後,依次執行 enter 和 after 類回調函數:

// NOTE: 執行 enter 鉤子
f.enterStateCallbacks(ctx, e)
// NOTE: 執行 after 鉤子
f.afterEventCallbacks(ctx, e)

根據 Event 方法的源碼走讀,我們可以總結出狀態轉換的核心流程如下:

FSM Event

本小節最後再貼一下 Transition 方法的源碼:

// Transition wraps transitioner.transition.
func (f *FSM) Transition() error {
    f.eventMu.Lock()
    defer f.eventMu.Unlock()
    return f.doTransition()
}
回調函數

現在,我們來看一下回調函數的具體實現:

// beforeEventCallbacks calls the before_ callbacks, first the named then the
// general version.
func (f *FSM) beforeEventCallbacks(ctx context.Context, e *Event) error {
    if fn, ok := f.callbacks[cKey{e.Event, callbackBeforeEvent}]; ok {
        fn(ctx, e)
        if e.canceled {
            return CanceledError{e.Err}
        }
    }
    if fn, ok := f.callbacks[cKey{"", callbackBeforeEvent}]; ok {
        fn(ctx, e)
        if e.canceled {
            return CanceledError{e.Err}
        }
    }
    returnnil
}

// leaveStateCallbacks calls the leave_ callbacks, first the named then the
// general version.
func (f *FSM) leaveStateCallbacks(ctx context.Context, e *Event) error {
    if fn, ok := f.callbacks[cKey{f.current, callbackLeaveState}]; ok {
        fn(ctx, e)
        if e.canceled {
            return CanceledError{e.Err}
        } elseif e.async { // NOTE: 異步信號
            return AsyncError{Err: e.Err}
        }
    }
    if fn, ok := f.callbacks[cKey{"", callbackLeaveState}]; ok {
        fn(ctx, e)
        if e.canceled {
            return CanceledError{e.Err}
        } elseif e.async {
            return AsyncError{Err: e.Err}
        }
    }
    returnnil
}

// enterStateCallbacks calls the enter_ callbacks, first the named then the
// general version.
func (f *FSM) enterStateCallbacks(ctx context.Context, e *Event) {
    if fn, ok := f.callbacks[cKey{f.current, callbackEnterState}]; ok {
        fn(ctx, e)
    }
    if fn, ok := f.callbacks[cKey{"", callbackEnterState}]; ok {
        fn(ctx, e)
    }
}

// afterEventCallbacks calls the after_ callbacks, first the named then the
// general version.
func (f *FSM) afterEventCallbacks(ctx context.Context, e *Event) {
    if fn, ok := f.callbacks[cKey{e.Event, callbackAfterEvent}]; ok {
        fn(ctx, e)
    }
    if fn, ok := f.callbacks[cKey{"", callbackAfterEvent}]; ok {
        fn(ctx, e)
    }
}

細心觀察,你會發現這幾個回調函數邏輯其實套路一樣,都是先匹配 cKey 的 target 值爲 e.Event 回調函數來執行,然後再匹配 target 值爲 "" 的回調函數來執行。

還記得 target 何時纔會爲空嗎?我們一起回顧下 NewFSM 中的代碼段:

// 根據回調函數名稱前綴分類
switch {
// 事件觸發前執行
case strings.HasPrefix(name, "before_"):
    target = strings.TrimPrefix(name, "before_")
    if target == "event" { // 全局事件前置鉤子(任何事件觸發都會調用,如用於日誌記錄場景)
        target = ""// 將 target 置空
        callbackType = callbackBeforeEvent
    } elseif _, ok := allEvents[target]; ok { // 在特定事件前執行
        callbackType = callbackBeforeEvent
    }
// 離開當前狀態前執行
case strings.HasPrefix(name, "leave_"):
    target = strings.TrimPrefix(name, "leave_")
    if target == "state" { // 全局狀態離開鉤子
        target = ""
        callbackType = callbackLeaveState
    } elseif _, ok := allStates[target]; ok { // 離開舊狀態前執行
        callbackType = callbackLeaveState
    }

當 target 的值爲 event/state 是,就會標記爲 ""

所以,我們可以得出結論:xxx_event 或 xxx_state 回調函數,會晚於 xxx_<EVENT> 或 xxx_<STATE> 而執行。

那麼,至此我們就理清了狀態轉換時所有的回調函數執行順序:

FSM Event

而這一結論,與我們在上一篇文章中講解的示例程序執行輸出結果保持一致:

FSM Event

此外,不知道你有沒有發現,其實我在上一篇文章中挖了一個坑沒有詳細講解。

在前一篇文章中,我們定義瞭如下狀態轉換規則:

fsm.Events{
    {Name: "open", Src: []string{"closed"}, Dst: "open"},
    {Name: "close", Src: []string{"open"}, Dst: "closed"},
},

細心的你可能已經發現,其實第一條規則中,事件和目標狀態,都叫 open;而第二條規則中,事件叫 close,目標狀態叫 closed

那麼你有沒有思考過,當事件和目標狀態同名時,即在這裏 open 既是 event 又是 state,那麼定義如下回調函數,這個回調函數是屬於 event 還是 state 呢?

"open": func(_ context.Context, e *fsm.Event) {
    color.Green("| enter open\t | %s | %s |", e.Src, e.Dst)
},

我們知道,<NEW_STATE> 是 enter_<NEW_STATE> 的簡寫形式,而 <EVENT> 又是 after_<EVENT> 的簡寫形式。

我們還知道,這段邏輯是在 NewFSM 中的 default case 代碼中實現的:

// 處理未加前綴的回調(簡短版本)
default:
    target = name                       // 狀態/事件
    if _, ok := allStates[target]; ok { // 如果 target 爲某個狀態,則 callbackType 會置爲與 enter_[target] 相同
        callbackType = callbackEnterState
    } else if _, ok := allEvents[target]; ok { // 如果 target 爲某個事件,則 callbackType 會置爲與 after_[target] 相同
        callbackType = callbackAfterEvent
    }

而這段代碼中,優先使用 allStates[target] 來匹配 target,即 open 會優先當作 state 來處理。

至此,關於回調函數的全部邏輯纔算梳理完成。

元信息

FSM 對於元信息的操作非常簡單,所有涉及元信息操作的方法源碼如下:

// Metadata 返回存儲在元信息中的值
func (f *FSM) Metadata(key string) (interface{}, bool) {
    f.metadataMu.RLock()
    defer f.metadataMu.RUnlock()
    dataElement, ok := f.metadata[key]
    return dataElement, ok
}

// SetMetadata 存儲 key、val 到元信息中
func (f *FSM) SetMetadata(key string, dataValue interface{}) {
    f.metadataMu.Lock()
    defer f.metadataMu.Unlock()
    f.metadata[key] = dataValue
}

// DeleteMetadata 從元信息中刪除指定 key 對應的數據
func (f *FSM) DeleteMetadata(key string) {
    f.metadataMu.Lock()
    delete(f.metadata, key)
    f.metadataMu.Unlock()
}

至於元信息有什麼用,我將用一個示例進行講解。

使用示例

對於 FSM 的元信息和異步狀態轉換操作,僅通過閱讀源碼,可能無法體會其使用場景。本小節將分別使用兩個示例對其進行演示,以此來加深你的理解。

元信息使用

對於有限狀態機中元信息的使用,我寫了一個使用示例:

https://github.com/jianghushinian/blog-go-example/blob/main/fsm/examples/data/data.go

package main

import (
    "context"
    "fmt"

    "github.com/looplab/fsm"
)

// NOTE: 將 FSM 作爲生產者消費者使用

func main() {
    fsm := fsm.NewFSM(
        "idle",
        fsm.Events{
            // 生產者
            {Name: "produce", Src: []string{"idle"}, Dst: "idle"},
            // 消費者
            {Name: "consume", Src: []string{"idle"}, Dst: "idle"},
            // 清理數據
            {Name: "remove", Src: []string{"idle"}, Dst: "idle"},
        },
        fsm.Callbacks{
            // 生產者
            "produce": func(_ context.Context, e *fsm.Event) {
                dataValue := "江湖十年"
                e.FSM.SetMetadata("message", dataValue)
                fmt.Printf("produced data: %s\n", dataValue)
            },
            // 消費者
            "consume": func(_ context.Context, e *fsm.Event) {
                data, ok := e.FSM.Metadata("message")
                if ok {
                    fmt.Printf("consume data: %s\n", data)
                }
            },
            // 清理數據
            "remove": func(_ context.Context, e *fsm.Event) {
                e.FSM.DeleteMetadata("message")
                if _, ok := e.FSM.Metadata("message"); !ok {
                    fmt.Println("removed data")
                }
            },
        },
    )

    fmt.Printf("current state: %s\n", fsm.Current())

    err := fsm.Event(context.Background(), "produce")
    if err != nil {
        fmt.Printf("produce err: %s\n", err)
    }

    fmt.Printf("current state: %s\n", fsm.Current())

    err = fsm.Event(context.Background(), "consume")
    if err != nil {
        fmt.Printf("consume err: %s\n", err)
    }

    fmt.Printf("current state: %s\n", fsm.Current())

    err = fsm.Event(context.Background(), "remove")
    if err != nil {
        fmt.Printf("remove err: %s\n", err)
    }

    fmt.Printf("current state: %s\n", fsm.Current())
}

在這個示例中,將 FSM 作爲了生產者消費者來使用。而數據的傳遞,正是通過元信息(FSM.metadata)來實現的。

執行示例代碼,得到輸出如下:

$ go run examples/data/data.go
current state: idle
produced data: 江湖十年
produce err: no transition
current state: idle
consume data: 江湖十年
consume err: no transition
current state: idle
removed data
remove err: no transition
current state: idle

可以發現,在數據的傳遞過程中,我們得到了 no transition 錯誤,而這個錯誤其實我們之前有解讀過,是在 Event 方法如下代碼段中產生的:

// NOTE: 當前狀態等於目標狀態,無需轉換
if f.current == dst {
    f.stateMu.RUnlock()
    defer f.stateMu.RLock()
    f.eventMu.Unlock()
    unlocked = true
    // NOTE: 執行 after 鉤子
    f.afterEventCallbacks(ctx, e)
    return NoTransitionError{e.Err}
}

因爲 FSM 的狀態始終是 idle,尚未發生狀態轉換,所以會返回 NoTransitionError 這個 Sentinel Error。

所以,我們只需要忽略這個 NoTransitionError,那麼就能把狀態機 FSM 當作生產者消費者來使用。

當然要實現生產者消費者功能我們有很多其他的選擇,這個示例主要是作爲演示,讓我們能夠清晰的知道 FSM 提供的元信息功能如何使用。

異步示例

在 FSM 源碼解讀的過程中,我有意避而不談異步狀態轉換。是因爲沒有示例的講解,直接閱讀源碼,不太容易理解。

我在這裏爲你演示一個示例,讓你來體會一下異步狀態轉換的用法:

https://github.com/jianghushinian/blog-go-example/blob/main/fsm/examples/async/async_transition.go

package main

import (
    "context"
    "errors"
    "fmt"

    "github.com/looplab/fsm"
)

// NOTE: 異步狀態轉換

func main() {
    // 構造有限狀態機
    f := fsm.NewFSM(
        "start",
        fsm.Events{
            {Name: "run", Src: []string{"start"}, Dst: "end"},
        },
        fsm.Callbacks{
            // 註冊 leave_<OLD_STATE> 回調函數
            "leave_start": func(_ context.Context, e *fsm.Event) {
                e.Async() // NOTE: 標記爲異步,觸發事件時不進行狀態轉換
            },
        },
    )

    // NOTE: 觸發 run 事件,但不會完整狀態轉換
    err := f.Event(context.Background(), "run")

    // NOTE: Sentinel Error `fsm.AsyncError` 標識異步狀態轉換
    var asyncError fsm.AsyncError
    ok := errors.As(err, &asyncError)
    if !ok {
        panic(fmt.Sprintf("expected error to be 'AsyncError', got %v", err))
    }

    // NOTE: 主動執行狀態轉換操作
    if err = f.Transition(); err != nil {
        panic(fmt.Sprintf("Error encountered when transitioning: %v", err))
    }

    // NOTE: 當前狀態
    fmt.Printf("current state: %s\n", f.Current())
}

示例中,在構造有限狀態機對象 f 時,爲其註冊了 leave_start 回調函數,這個回調函數是異步狀態轉換的關鍵所在。其內部通過 e.Async() 將事件標記爲異步,這樣在事件觸發時,就不會執行狀態轉換邏輯。

接着,代碼中觸發 run 事件。不過由於 e.Async() 的操作,事件觸發時不會進行狀態轉換,而是返回 Sentinel Error fsm.AsyncError,這個錯誤用於標識這是一個異步操作,尚未進行狀態轉換。

接下來,我們主動調用 f.Transition() 來執行狀態轉換操作。

最終,打印 FSM 當前狀態。

執行示例代碼,得到輸出如下:

$ go run examples/async/async_transition.go 
current state: end

這個玩法,將觸發事件和狀態轉換操作進行了分離,使得我們可以主動控制狀態轉換的時機。

這個示例的關鍵步驟是在 leave_start 回調函數中的 e.Async() 邏輯,將當前事件標記爲了異步。

首先,Event 對象其實也是一個結構體,它有一個屬性 asynce.Async() 邏輯如下:

func (e *Event) Async() {
    e.async = true
}

而 leave_start 回調函數,是在調用 *FSM.Event 方法時觸發的:

// NOTE: 執行 leave 鉤子
if err = f.leaveStateCallbacks(ctx, e); err != nil {
    if _, ok := err.(CanceledError); ok {
        f.transition = nil// NOTE: 如果通過 ctx 取消了,則標記爲 nil,無需轉換
    } elseif asyncError, ok := err.(AsyncError); ok { // NOTE: 如果是 AsyncError,說明是異步轉換
        // 爲異步操作創建獨立上下文,以便異步狀態轉換正常工作
        // 這個新的 ctx 實際上已經脫離了原始 ctx,原 ctx 取消不會影響當前 ctx
        // 不過新的 ctx 保留了原始 ctx 的值,所有通過 ctx 傳遞的值還可以繼續使用
        ctx, cancel := uncancelContext(ctx)
        e.cancelFunc = cancel                    // 綁定新取消函數
        asyncError.Ctx = ctx                     // 傳遞新上下文
        asyncError.CancelTransition = cancel     // 暴露取消接口
        f.transition = transitionFunc(ctx, true) // NOTE: 標記爲異步轉換狀態
        // NOTE: 如果是異步轉換,直接返回,不會同步調用 f.doTransition(),需要用戶手動調用 f.Transition() 來觸發狀態轉換
        return asyncError
    }
    return err
}

f.leaveStateCallbacks 就是在執行 leave_start 回調函數,其實現如下:

func (f *FSM) leaveStateCallbacks(ctx context.Context, e *Event) error {
    if fn, ok := f.callbacks[cKey{f.current, callbackLeaveState}]; ok {
        fn(ctx, e)
        if e.canceled {
            return CanceledError{e.Err}
        } else if e.async { // NOTE: 異步信號
            return AsyncError{Err: e.Err}
        }
    }
    ...
    return nil
}

這裏最關鍵的一步就是在 else if e.async 時,返回 Sentinel Error AsyncError

而對 f.leaveStateCallbacks(ctx, e) 的調用一旦返回 AsyncError,就說明是要進入異步狀態轉換邏輯。

此時會爲 f.transition 重新賦值,並標記爲異步狀態轉換:

f.transition = transitionFunc(ctx, true) // NOTE: 標記爲異步轉換狀態
// NOTE: 如果是異步轉換,直接返回,不會同步調用 f.doTransition(),需要用戶手動調用 f.Transition() 來觸發狀態轉換
return asyncError

並且返回 asyncError,這次 Event 事件觸發就完成了。不過並沒有接着去執行 f.transition() 邏輯。所以就實現了異步操作。

到這裏,異步轉換狀態的邏輯,我就幫你梳理完成了。這塊可能不太好理解,但是你跟着我的思路,執行一遍示例代碼,然後深入到源碼,按照流程再梳理一遍,相信就就一定能理解了。

總結

上一篇文章我們一起學習瞭如何利用有限狀態機 FSM 實現程序中的狀態轉換。

本篇文章我帶你完整閱讀了有限狀態機的核心源碼,爲你理清了 FSM 的設計思路和它提供的能力。讓你能夠知其然,也能知其所以然。

並且我還針對不太常用的元信息操作和異步狀態轉換,提供了使用示例。其實官方 examples 中提供了好幾個示例,你可以自行看一下,學完了本文源碼,再去看示例就是小菜一碟的事情了。

值得注意的是,因爲所有的狀態轉換核心邏輯都加了互斥鎖,所以 FSM 是併發安全的。

本文示例源碼我都放在了 GitHub 中,歡迎點擊查看。

希望此文能對你有所啓發。

延伸閱讀

聯繫我

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/UhCST89W6vBJsvUTQFP2Vg