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
}
我們知道,有限狀態機中最重要的三個特徵如下:
-
狀態(state)個數是有限的。
-
任意一個時刻,只處於其中一種狀態。
-
某種條件下(觸發某種 event),會從一種狀態轉變(transition)爲另一種狀態。
所以,<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
)來實現的。
-
FSM.SetMetadata
用於設置元信息。 -
FSM.Metadata
用於獲取元信息。 -
FSM.DeleteMetadata
則用於清理元信息。
執行示例代碼,得到輸出如下:
$ 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
對象其實也是一個結構體,它有一個屬性 async
,e.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 中,歡迎點擊查看。
希望此文能對你有所啓發。
延伸閱讀
-
有限狀態機定義:https://zh.wikipedia.org/wiki / 有限狀態機
-
JavaScript 與有限狀態機:https://www.ruanyifeng.com/blog/2013/09/finite-state_machine_for_javascript.html
-
OneX 有限狀態機:https://github.com/onexstack/onex/tree/feature/onex-v2/internal/nightwatch/watcher/user
-
fsm GitHub 源碼:https://github.com/looplab/fsm
-
fsm Documentation:https://pkg.go.dev/github.com/looplab/fsm@v1.0.3
-
在 Go 中如何使用有限狀態機優雅解決程序中狀態轉換問題:https://jianghushinian.cn/2025/05/25/fsm/
-
本文 GitHub 示例代碼:https://github.com/jianghushinian/blog-go-example/tree/main/fsm
-
本文永久地址:https://jianghushinian.cn/2025/06/01/fsm-source-code/
聯繫我
-
公衆號:Go 編程世界
-
微信:jianghushinian
-
郵箱:jianghushinian007@outlook.com
-
博客:https://jianghushinian.cn
-
GitHub:https://github.com/jianghushinian
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/UhCST89W6vBJsvUTQFP2Vg