FSM provides a lightweight finite state machine for Golang. It runs allows any number of transition checks you'd like the it runs them in parallel. It's tested and benchmarked too.
go get github.com/ryanfaerman/fsm
package main
import (
    "log"
    "fmt"
    "github.com/ryanfaerman/fsm"
)
type Thing struct {
    State fsm.State
    // our machine cache
    machine *fsm.Machine
}
// Add methods to comply with the fsm.Stater interface
func (t *Thing) CurrentState() fsm.State { return t.State }
func (t *Thing) SetState(s fsm.State)    { t.State = s }
// A helpful function that lets us apply arbitrary rulesets to this
// instances state machine without reallocating the machine. While not
// required, it's something I like to have.
func (t *Thing) Apply(r *fsm.Ruleset) *fsm.Machine {
    if t.machine == nil {
        t.machine = &fsm.Machine{Subject: t}
    }
    t.machine.Rules = r
    return t.machine
}
func main() {
    var err error
    some_thing := Thing{State: "pending"} // Our subject
    fmt.Println(some_thing)
    // Establish some rules for our FSM
    rules := fsm.Ruleset{}
    rules.AddTransition(fsm.T{"pending", "started"})
    rules.AddTransition(fsm.T{"started", "finished"})
    err = some_thing.Apply(&rules).Transition("started")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(some_thing)
}Note: FSM makes no effort to determine the default state for any ruleset. That's your job.
The Apply(r *fsm.Ruleset) *fsm.Machine method is absolutely optional. I like having it though. It solves a pretty common problem I usually have when working with permissions - some users aren't allowed to transition between certain states.
Since the rules are applied to the the subject (through the machine) I can have a simple lookup to determine the ruleset that the subject has to follow for a given user. As a result, I rarely need to use any complicated guards but I can if need be. I leave the lookup and the maintaining of independent rulesets as an exercise of the user.
Golang makes it easy enough to benchmark things... why not do a few general benchmarks?
$ go test -bench=.
PASS
BenchmarkRulesetParallelGuarding      100000   13163 ns/op
BenchmarkRulesetTransitionPermitted  1000000    1805 ns/op
BenchmarkRulesetTransitionDenied    10000000     284 ns/op
BenchmarkRulesetRuleForbids          1000000    1717 ns/op
ok    github.com/ryanfaerman/fsm 14.864sI think that's pretty good. So why do I go through the trouble of running the guards in parallel? Consider the benchmarks below:
$ go test -bench=.
PASS
BenchmarkRulesetParallelGuarding    100000       13261 ns/op
ok  github.com/ryanfaerman/fsm 1.525s
$ go test -bench=.
PASS
BenchmarkRulesetSerialGuarding         1  1003140956 ns/op
ok  github.com/ryanfaerman/fsm 1.007sFor the Parallel vs Serial benchmarks I had a guard that slept for 1 second. While I don't imagine most guards will take that long, the point remains true. Some guards will be comparatively slow -- they'll be accessing the database or consulting with some other outside service -- and why not get an answer back as soon as possible?
