用Go实现一个状态机

简介: 工作中,很多同学会用到状态机,例如对一个工单进行创建、编辑、审核,在执行新动作前,要检查能否从当前状态流转到下一个状态。对这种需求,我们怎么实现呢?

工作中,很多同学会用到状态机,例如对一个工单进行创建、编辑、审核,在执行新动作前,要检查能否从当前状态流转到下一个状态。对这种需求,我们怎么实现呢?

数组

Go设计模式(22)-状态模式中说过,简单的状态管理使用数组即可完成,无需使用状态模式。以下图为例,状态之间的流转,无法跳跃,即只能从编辑中跳到提交,无法直接从编辑中跳到审核通过。

图片

这种实现方案很简单,配置数组用于检查:

func (s *shopWarehouse) statusCheck(currentStatus, targetStatus int64) bool {
    //target -> currents
    statusMap := map[int64][]int64{
        consts.Editing: []int64{consts.Editing, consts.Pass, consts.Reject},
        consts.Submit:  []int64{consts.Editing, consts.Reject},
        consts.Pass:    []int64{consts.Submit},
        consts.Reject:  []int64{consts.Submit},
        consts.Invalid: []int64{consts.Pass},
        consts.Delete:  []int64{consts.Reject, consts.Editing},
    }
    if statusList, ok := statusMap[targetStatus]; ok {
        if util.InArray(currentStatus, statusList) {
            return true
        }
        return false
    } else {
        return false
    }
}

数组在设置上,可以设置为target->current,也可设置为为current->target,current->target更易于理解。

状态检查需要两步,先查询当前状态,然后调用statusCheck函数,但两个操作之间,若有其它线程更改了当前状态呢?

其实可以用Go中加锁的例子进行解释,在Go锁,我终于搞懂了中,加互斥锁也分两步,获取当前状态,然后执行CAS,必然会遇到并发情况,对当前状态进行了变更。但没有影响,因为只要更新的那一刻,我们关注的数据状态没有变化,那就说明变更合法的,直接更新即可。

将状态放到数组里统一维护,比将各个状态分散开判断,在设计上要好得多。


-

上面的流转比较简单,而且没有跳跃情况。如果状态十分多,而且会跳跃,再用数组方案,维护成本极高。

此时,图的作用就发挥出来了。分支限界法回溯法里有很多广度优先遍历和深度优先遍历的例子,希望可以帮助大家回忆起相关算法。

这里先举一个状态流转例子:

图片

其中红色表示必须经过的节点,白色可以跳过,对于这种情况,我们应该怎样实现呢?

package main

import (
   "fmt"
   "reflect"
)

// 时序图
var StatusTimingGraph = map[string][]string{
   "A":  {"B1", "B2"},
   "B1": {"C1", "C2"},
   "B2": {"B1"},
   "C1": {"D"},
   "C2": {"C1"},
   "D":  {"E"},
}

// 核心节点
var CoreStatus = []string{
   "A",
   "B1",
   "C1",
   "E",
}
var StatusJumpGraph = InitJumpGraph(StatusTimingGraph, CoreStatus)

func InitJumpGraph(statusMap map[string][]string, coreStatus []string) map[string][]string {
   retMap := make(map[string][]string, 0)
   for status, statusList := range statusMap {
      retList := make([]string, 0)
      for _, tStatus := range statusList {
         retList = append(retList, tStatus)
         if InSlice(coreStatus, tStatus) {
            continue
         }
         tList := recursionGraph(tStatus, statusMap, coreStatus)
         for _, tStatus := range tList {
            if !InSlice(retList, tStatus) {
               retList = append(retList, tStatus)
            }
         }
      }

      retMap[status] = retList
   }
   return retMap
}
func recursionGraph(status string, statusMap map[string][]string, coreStatus []string) []string {
   retList := make([]string, 0)
   if statusList, ok := statusMap[status]; ok {
      for _, tStatus := range statusList {
         retList = append(retList, tStatus)
         if InSlice(coreStatus, tStatus) {
            continue
         }
         retList = append(retList, recursionGraph(tStatus, statusMap, coreStatus)...)
      }
   }
   return retList
}
func InSlice(a, b interface{}) bool {
   exist, _ := InSliceWithError(a, b)
   return exist
}
func InSliceWithError(a, b interface{}) (exist bool, err error) {

   va := reflect.ValueOf(a)

   if va.Kind() != reflect.Slice {
      err = fmt.Errorf("parameter a must be a slice")
      return
   }

   if reflect.TypeOf(a).String()[2:] != reflect.TypeOf(b).String() {
      err = fmt.Errorf("type of parameter b not match with parameter a")
      return
   }

   for i := 0; i < va.Len(); i++ {
      if va.Index(i).Interface() == b {
         exist = true
         return
      }
   }

   return
}

func main() {
   originStatus := "A"
   targetStatus := "B1"
   statusList, ok := StatusJumpGraph[originStatus]
   fmt.Println(StatusJumpGraph)
   if !ok {
      fmt.Println("状态有误")
      return
   }

   if !InSlice(statusList, targetStatus) {
      fmt.Println("状态不合规,无法流转")
      return
   }
}

输出:

➜ myproject go run main.go

map[A:[B1 B2] B1:[C1 C2] B2:[B1] C1:[D E] C2:[C1] D:[E]]

这个代码其实是深度优先遍历,CoreStatus意味深度优先遍历终止的位置。使用该方案,无论状态图多复杂,只需修改状态配置即可。

但这个方案有一个问题,正好可以留给大家思考:如果是幂等情况,需要如何实现?即D->D的情况。

资料

  1. UML:https://www.processon.com/view/link/6174cb1b63768912b562ce29

最后

大家如果喜欢我的文章,可以关注我的公众号(程序员麻辣烫)

我的个人博客为:https://shidawuhen.github.io/

往期文章回顾:

  1. 设计模式
  2. 招聘
  3. 思考
  4. 存储
  5. 算法系列
  6. 读书笔记
  7. 小工具
  8. 架构
  9. 网络
  10. Go语言
相关文章
|
Go 数据库 数据安全/隐私保护
Go实现随机加盐密码认证
Go实现随机加盐密码认证
313 0
|
存储 缓存 人工智能
基于Go的缓存实现
缓存是架构设计中的常用概念,本文基于Go实现了一个简单的缓存组件,支持最基本的缓存操作。
279 0
基于Go的缓存实现
|
存储 缓存 NoSQL
一文搞懂Go整合captcha实现验证码功能
一文搞懂Go整合captcha实现验证码功能
|
XML JSON Java
RPC框架之Thrift—实现Go和Java远程过程调用
RPC框架之Thrift—实现Go和Java远程过程调用
|
监控 测试技术 Go
用 Go 从零实现日志包 - 第零篇 序言
设计一个日志包,需要考虑的基础功能有日志级别设置、标准输出和文件、输出格式配置、日志的时间戳、文件与打印行号、正文。高级功能有按级别分类输出、支持结构化日志、支持日志轮转。
138 0
|
Go 数据安全/隐私保护
Go 实现 AES 加密 CBC 模式|Go主题月
密码分组链接模式 CBC (Cipher Block Chaining),这种模式是先将明文切分成若干小段,然后每一小段与初始块或者上一段的密文段进行异或运算后,再与密钥进行加密。
350 0
|
Go
【Golang】panic和recover底层逻辑实现|Go主题月
在每个goroutine也有一个指针指向_panic链表表头,然后每增加一个panic就会在链表头部加入一个_panic结构体。当所有的defer执行完后,_panic链表就会从尾部开始打印panic信息了,也就是说先发生的panic先打印信息。
239 0
|
Go 索引 Python
Go 和Python中的闭包实现及使用
Go 和Python中的闭包实现及使用
125 0
go语言实现【队列】|二叉树的【先序遍历】【创建】
go语言实现【队列】|二叉树的【先序遍历】【创建】
go语言实现【队列】|二叉树的【先序遍历】【创建】
|
Java Go
Go语言实现多态
Go语言实现多态
253 0
Go语言实现多态