Go - State Machine Pattern
Updated at 2013-12-02 10:08
Finite state machines can easily made with Go. Here, the communication with the state machine is done with strings, but they can be anything.
package main
import "fmt"
// Parameters: next state, error that happened, is this machine finished.
type State func(msg string) (State, error, bool)
type StateMachine struct {
// Any extra data needed for the state machine.
}
func (self *StateMachine) StateOne(msg string) (State, error, bool) {
fmt.Println("ONE STATE!")
return self.StateTwo, nil, false
}
func (self *StateMachine) StateTwo(msg string) (State, error, bool) {
fmt.Println("TWO STATE!")
if msg == "exit now" {
return nil, nil, true
}
return self.StateOne, nil, false
}
func (self *StateMachine) Run(msgs chan string, shutToKill chan bool) {
currentState := self.StateOne
var err error = nil
finished := false
for msg := range msgs {
currentState, err, finished = currentState(msg)
if finished {
close(shutToKill)
}
if err != nil {
close(shutToKill)
}
}
close(shutToKill)
}
func main() {
messages := make(chan string)
shutToKill := make(chan bool)
var machine StateMachine
go machine.Run(messages, shutToKill)
messages <- "stuff"
messages <- "more stuff"
messages <- "nothing is happening"
messages <- "exit now"
<-shutToKill
fmt.Println("We are done.")
}
Next, a slightly more complex example with string line based protocol that creates a transaction from multiple commands.
package main
import (
"fmt"
"strconv"
)
type State func(msg string) (State, error, bool)
type TransactionMachine struct {
commands [][]string
command []string
commandsCount int
argumentCount int
}
func (self *TransactionMachine) CommandsCount(line string) (State, error, bool) {
fmt.Print("Counting commands... ")
count, err := strconv.Atoi(line)
fmt.Println(count)
if err != nil {
return nil, err, false
}
self.commandsCount = count
return self.ArgumentCount, nil, false
}
func (self *TransactionMachine) ArgumentCount(line string) (State, error, bool) {
fmt.Print("Counting arguments... ")
count, err := strconv.Atoi(line)
fmt.Println(count)
if err != nil {
return nil, err, false
}
self.argumentCount = count
return self.Command, nil, false
}
func (self *TransactionMachine) Command(line string) (State, error, bool) {
fmt.Println("Reading command...")
self.command = []string{line}
if self.argumentCount == 0 {
self.commands = append(self.commands, self.command)
if len(self.commands) == self.commandsCount {
return nil, nil, true
}
}
return self.Argument, nil, false
}
func (self *TransactionMachine) Argument(line string) (State, error, bool) {
fmt.Println("Reading argument...")
self.command = append(self.command, line)
if len(self.command) == self.argumentCount + 1 {
self.commands = append(self.commands, self.command)
if len(self.commands) == self.commandsCount {
return nil, nil, true
}
return self.ArgumentCount, nil, false
}
return self.Argument, nil, false
}
func (self *TransactionMachine) Run(msgs chan string, shutToKill chan bool) {
var currentState = self.CommandsCount
var err error = nil
var finished = false
for msg := range msgs {
currentState, err, finished = currentState(msg)
if finished {
close(shutToKill)
}
if err != nil {
close(shutToKill)
}
}
close(shutToKill)
}
func main() {
commands := make(chan string)
shutToKill := make(chan bool)
var machine TransactionMachine
go machine.Run(commands, shutToKill)
commands <- "3" // We have 3 commands.
commands <- "2" // First one is a SET with 2 arguments.
commands <- "SET"
commands <- "valuex"
commands <- "1"
commands <- "2" // Second one is a SET with 2 arguments.
commands <- "SET"
commands <- "valuey"
commands <- "9"
commands <- "1" // Third one is a DELETE with 1 argument.
commands <- "DELETE"
commands <- "valuey"
<-shutToKill
fmt.Println(machine.commands)
}