[翻译] Go(#golang) 实现的状态机

春节前,粗略研究了一下微信的公共帐号。用 Golang 实现了一个简单的 package wechat,用于接入微信公共帐号。当时就在思考,微信的文字交互过程如果要实现有一定逻辑的复杂过程,可能需要使用到状态机。然后,就看到了这篇文章:《State machines in Go (#golang)》。非常合时宜啊!翻译于此,以飨读者!

——–翻译分隔线——–

Go(#golang) 实现的状态机

我已经用 Go 代替 Python 重写了一个关键的服务组件。由于 Python 的解释器不是线程安全的,所以在解析的时候使用了全局锁。Go 与 Python 不同,它内建了并发支持,并且是静态编译的。

首先要实现一个状态机Python 的版本是基于 David Mertz 的这篇文章

Mertz 使用了面向对象的形式,定义了一个有着数据和方法的类。他的代码,抛开语法不谈,对于任何有着 C++、C# 和 Java 的面向对象经验的人来说都不会陌生。

不过 Go 没有提供在特定数据结构上内部关联方法的机制。作为代替,Go 允许联合方法到数据结构,这样任何方法都可以应用到任何结构上。(译注:class { methods } 和 struct { }; methods 的区别。)

这种形式与 Alan Kay 所表达的,关于最初的面向对象阶段比较接近。

先别忘了这些,下面是我最开始用 Go 结构体编写的状态机的类:

type Machine struct {
    Handlers   map[string]func(interface{}) (string, interface{})
    StartState string
    EndStates  map[string]bool
}

跟 Mertz 的定义一样,Handlers 是一个用 string 做键名的 map,map 项保存的值是函数,可以接收一个“物料”,并且返回下一个状态名的字符串和更新后的物料值。

Go 认为函数是一等公民对象,因此将它们在状态之间存储和传递跟在 Python 中的方式一样。

我仅仅在状态列表的最后做了一些改变:Mertz 使用一个字符串的列表,但由于没有办法在 Go 的列表中进行快速的定位,我使用了 map(在 Go 中,只能通过迭代遍历整个字符串列表,直到找到一个匹配项)。

由于处理函数的原型比较笨重,我为其建立了一个自定义函数类型

type Handler func(interface{}) (string, interface{})

type Machine struct {
    Handlers   map[string]Handler
    StartState string
    EndStates  map[string]bool
}

剩下的就是定义 Machine 结构体关联的方法。

首先定义的两个方法,一个提供了状态名关联到处理函数,另一个设定了结束状态:

func (machine *Machine) AddState(handlerName string, handlerFn Handler) {
    machine.Handlers[handlerName] = handlerFn
}

func (machine *Machine) AddEndState(endState string) {
    machine.EndStates[endState] = true
}

值得说明的是由于 EndStates 是一个 map(在 Mertz 原始的版本中是 list),所以可以有多个终止处理过程的状态。

最后一个方法用于执行状态机,应用恰当的处理函数,并在到达结束状态时终止。

由于函数集合作为一等公民对象保存在 map 中,基于名字找到它们并且进行调用是很轻松的:

func (machine *Machine) Execute(cargo interface{}) {
    if handler, present := machine.Handlers[machine.StartState]; present {
        for {
            nextState, nextCargo := handler(cargo)
            _, finished := machine.EndStates[nextState]
            if finished {
                break
            } else {
                handler, present = machine.Handlers[nextState]
                cargo = nextCargo
            }
        }
    }
}

唯一美中不足的是 Go 的强类型,在处理函数的原型中,需要指定物料的类型。

使用通用的 interface{} 作为类型,所有处理函数都需要对输入的物料进行类型断言,这样它们就可以处理任何数据(测试的例子使用了浮点作为物料,不过其实它可以是任何数据类型,甚至是自定义的结构体)。

完整的状态机已经作为 Go 包发布

Join the Conversation

3 Comments

  1. 请教一下,他实现的状态机是自己是一个server。而http包的ListenAndServe自己就在无线循环了,怎么把这两者整合起来呢?

Leave a comment

Your email address will not be published. Required fields are marked *