如何用Go实现一个状态机

3,056 阅读4分钟

前言

有限状态机(FSM)是表示某个业务对象,有有限个状态,以及在这些状态之间的转移和动作等行为的计算模型

现实生活中状态随处可见,例如门有opened,closed两种状态,当满足一定条件时,可以从opened到closed,也可以从closed到opened,并执行一些行为

使用FSM进行编程,有以下优点:

  • 提前定义清楚业务对象的所有状态,哪些状态之间可以转移,转移时要做什么操作
  • 在定义阶段对整个状态转移过程一目了然收敛状态转移操作

通常状态机一般有两种思路:

  1. 给定当前状态,事件,由状态机框架判断要转移到哪个状态,并执行转移操作
  2. 提前根据事件判定好要转移到哪个状态,给定当前状态,要转移的状态,执行转移操作。这样整个框架比较轻量

本文介绍第二种思路的一个简易的状态机实现

整体流程

  1. 项目启动时,将每种业务,所有状态之间要进行的转换操作注册到一个全局配置中

image.png

  1. 处理业务请求时,根据参数和当前状态,计算出要转移到的状态
  2. 执行转移操作

代码

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中添加通用前置后置处理函数;增加对第一种实现思路:给定当前状态,事件,由状态机框架判断要转移到哪个状态,并执行转移操作 的支持