Go 語言 fsm 源碼解讀,這一次讓你徹底學會有限狀態機
本篇文章,我將更進一步,直接通過解讀源碼的方式,讓你深刻理解 fsm 是如何實現的,這一次你將徹底掌握有限狀態機。
源碼解讀
廢話不多說,我們直接上代碼。
結構體
首先 fsm 包定義了一個結構體 FSM 用來表示狀態機。
// 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 style="color:rgb(33, 33, 33);">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 = ""http:// 將 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 = ""http:// 將 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 = ""http:// 將 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 的設計思路和它提供的能力。讓你能夠知其然,也能知其所以然。
并且我還針對不太常用的元信息操作和異步狀態轉換,提供了使用示例。其實官方 examples 中提供了好幾個示例,你可以自行看一下,學完了本文源碼,再去看示例就是小菜一碟的事情了。
值得注意的是,因為所有的狀態轉換核心邏輯都加了互斥鎖,所以 FSM 是并發安全的。