ruk·si

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)
}