如何用Go实现一个状态机
前言
有限状态机(FSM)是表示某个业务对象,有有限个状态,以及在这些状态之间的转移和动作等行为的计算模型
现实生活中状态随处可见,例如门有opened,closed两种状态,当满足一定条件时,可以从opened到closed,也可以从closed到opened,并执行一些行为
使用FSM进行编程,有以下优点:
- 提前定义清楚业务对象的所有状态,哪些状态之间可以转移,转移时要做什么操作
- 在定义阶段对整个状态转移过程一目了然,收敛状态转移操作
通常状态机一般有两种思路:
- 给定当前状态,事件,由状态机框架判断要转移到哪个状态,并执行转移操作
- 提前根据事件判定好要转移到哪个状态,给定当前状态,要转移的状态,执行转移操作。这样整个框架比较轻量
本文介绍第二种思路的一个简易的状态机实现
整体流程
- 项目启动时,将每种业务,所有状态之间要进行的转换操作注册到一个全局配置中
- 处理业务请求时,根据参数和当前状态,计算出要转移到的状态
- 执行转移操作
代码
const
type (
BusinessName string
State int64
ExecHandler func(context.Context, interface{}) (interface{}, error)
)
var ParamConvertInvalidError = errors.New("param convert invalid")
-
BusinessName:用于区分不同的业务
-
State:状态
-
ExecHandler:状态转移时需要执行的方法
- 该方法参数和返回值为inerface{}类型,因为需要不同业务需要的参数类型不同,相同业务不同的状态转移需要的参数也可能不同
- 这样在使用参数,使用返回值时,需要做类型转换
fsm
import (
"context"
"fmt"
)
type Fsm struct {
// 业务名
Business BusinessName
// 当前状态
CurrentState State
// 要转移到的状态
NextState State
}
type TransitionEntry struct {
// 执行状态转移的函数
Handler ExecHandler
}
// 新建状态机实例,需要指定业务名
func NewFsm(business BusinessName) *Fsm {
return &Fsm{
Business: business,
}
}
// 设置当前状态及要转移到的状态
func (f *Fsm) SetState(currentState, nextState int64) {
f.setCurrentState(State(currentState))
f.setNextState(State(nextState))
}
func (f *Fsm) setCurrentState(currentState State) {
f.CurrentState = currentState
}
func (f *Fsm) setNextState(nextState State) {
f.NextState = nextState
}
// 进行状态转移
func (f *Fsm) Transfer(ctx context.Context, param interface{}) (interface{}, error) {
stateMap, ok := stateMachineMap[f.Business]
if !ok {
return nil, fmt.Errorf("fsm business %v invalid", f.Business)
}
nextMap, ok := stateMap[f.CurrentState]
if !ok {
return nil, fmt.Errorf("fsm currentState %v invalid, business = %v", f.CurrentState, f.Business)
}
transitionEntry, ok := nextMap[f.NextState]
if !ok {
return nil, fmt.Errorf("fsm nextState %v invalid, business = %v", f.NextState, f.Business)
}
return transitionEntry.Handler(ctx, param)
}
本文件定义了Fsm的相关方法
-
NewFsm:初始化fsm实例,指定业务名
-
SetState:设置当前状态及要转移到的状态
-
Transfer:执行业务状态转移
- 从全局变量
stateMachineMap
中依次根据业务,当前状态,下一个状态,查找转移函数,并执行 - 若转移函数未注册,返回err
- 从全局变量
register
状态转移函数的注册
- 维护一个三层的全局
map
- 依次进度到业务名,当前状态,转移后的状态的子map中,注册状态转移函数
import "context"
var (
stateMachineMap = make(map[BusinessName]map[State]map[State]*TransitionEntry)
)
// 注册状态转移过程
func Register(bussiness BusinessName, currentStateInt64 int64, nextStateInt64 int64, handler ExecHandler) {
currentState := State(currentStateInt64)
nextState := State(nextStateInt64)
if stateMachineMap[bussiness] == nil {
stateMachineMap[bussiness] = make(map[State]map[State]*TransitionEntry)
}
if stateMachineMap[bussiness][currentState] == nil {
stateMachineMap[bussiness][currentState] = make(map[State]*TransitionEntry)
}
if stateMachineMap[bussiness][currentState][nextState] == nil {
stateMachineMap[bussiness][currentState][nextState] = &TransitionEntry{
Handler: handler,
}
}
}
// 定义什么也不做的函数
func DoNothing(ctx context.Context, req interface{}) (interface{}, error) {
return nil, nil
}
使用
- 在每个业务模块下,注册每种业务的每个状态相互转换需要执行的方法
- 以活动为例,假设有配置中,测试中,待上线三种状态,可以从配置中提交到测试中或待上线,可以从测试中转移到待上线,则进行如下注册:
const activityBase fsm.BusinessName = "activity"
func InisFsm() {
// 配置中 -> 待上线
fsm.Register(activityBase, int64(Configuring), int64(CanOnline), submitToConfig)
// 配置中 -> 测试中
fsm.Register(activityBase, int64(Configuring), int64(Testing), submitToTest)
// 测试中 -> 待上线
fsm.Register(activityBase, int64(Testing), int64(CanOnline), testDone)
}
注意在具体执行的方法中,需要检查参数类型是否符合预期:
// 其他文件中:
func testDone(ctx context.Context, req interface{}) (interface{}, error) {
// 检查参数类型
request, ok := req.(*testDoneFsmParam)
if !ok {
return 0, fsm.ParamConvertInvalidError
}
// 执行业务逻辑
// ...
}
-
执行业务
new
一个fsm实例,指定业务名- 设置当前状态和要转移到的状态
- 设置参数
- 执行转移操作
// new一个fsm实例
sm := fsm.NewFsm(activityBase)
// 设置状态
sm.SetState(int64(Testing), int64(CanOnline))
// 设置参数
arg := &testDoneFsmParam{
// ...
}
// 执行状态转移操作
resp,err := sm.Transfer(ctx, arg)
总结
本文介绍了如何用go时间一个简易状态机框架,在此基础上有可以改进的空间,例如在TransitionEntry
中添加通用前置后置处理函数;增加对第一种实现思路:给定当前状态,事件,由状态机框架判断要转移到哪个状态,并执行转移操作 的支持
转载自:https://juejin.cn/post/7090114093947830303