fsmgo is a clean, modular, and concurrent Go library for building finite state machines, inspired by FSMgasm and adapted with Go idioms for safe, real-time systems.
go get github.com/josscoder/fsmgoimport "github.com/josscoder/fsmgo/state"Every state has three lifecycle hooks:
| Hook | When it's called |
|---|---|
OnStart() |
Once, when the state begins |
OnUpdate(delta time.Duration) |
Every tick; receives real elapsed time |
OnEnd() |
Once, when the state finishes |
delta lets your state logic be frame-rate independent — subtract it from timers or use it to interpolate values rather than assuming a fixed interval.
Implement the PauseAware interface to receive pause/resume notifications. BaseState detects it automatically at construction time — no extra wiring needed.
type PauseAware interface {
OnPause()
OnResume()
}package states
import (
"fmt"
"time"
"github.com/josscoder/fsmgo/state"
)
type PrintState struct {
*state.BaseState
Text string
}
func NewPrintState(text string) *PrintState {
ps := &PrintState{Text: text}
ps.BaseState = state.NewBaseState(ps)
return ps
}
func (ps *PrintState) OnStart() { fmt.Println("Started:", ps.Text) }
func (ps *PrintState) OnUpdate(delta time.Duration) { fmt.Printf("tick %v — remaining: %v\n", delta, ps.GetRemainingTime()) }
func (ps *PrintState) OnEnd() { fmt.Println("Ended:", ps.Text) }
func (ps *PrintState) GetDuration() time.Duration { return 5 * time.Second }ps := states.NewPrintState("Hello World")
ps.Start()
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
var last time.Time
for t := range ticker.C {
delta := time.Second
if !last.IsZero() {
delta = t.Sub(last) // real elapsed time, absorbs scheduler jitter
}
last = t
ps.Update(delta)
if ps.HasEnded() {
return
}
}States run one after another. When the current state ends, the next one starts automatically.
series := state.NewStateSeries([]state.State{
states.NewPrintState("Step 1"),
states.NewPrintState("Step 2"),
})
series.Start()
// ... tick loop calling series.Update(delta)Series can be nested: a Series is itself a State, so it can be placed inside another Series or Group.
series.Skip() // skip the current state on the next tick
series.AddNext(newState) // insert a state right after the current one
series.AddNextList([]state.State{ // insert multiple states after current
stateA, stateB,
})All child states run concurrently. The group ends when every child has finished.
group := state.NewStateGroup([]state.State{
states.NewPrintState("Worker A"),
states.NewPrintState("Worker B"),
})
group.Start()
// ... tick loop calling group.Update(delta)Drives its own internal ticker so you don't need a manual update loop. Pass a context.Context to control its lifetime.
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
series := state.NewScheduledStateSeries(ctx, []state.State{
states.NewPrintState("Auto 1"),
states.NewPrintState("Auto 2"),
}, time.Second)
series.Start()
// Just wait — no Update calls needed.
for !series.HasEnded() {
time.Sleep(200 * time.Millisecond)
}Any state (or container) can be paused. Pause propagates to all children in a Group, Holder, or Series. If the state also implements PauseAware, OnPause / OnResume are called automatically.
ps := states.NewPausablePrintState("My State")
ps.Start()
// ... run a few ticks ...
ps.Pause() // countdown freezes; OnPause() called if PauseAware
time.Sleep(2 * time.Second)
ps.Resume() // countdown resumes; OnResume() called if PauseAwareAll public methods on BaseState and the built-in containers are safe to call from multiple goroutines. ScheduledStateSeries uses a context.Context for cancellation and sync.Once to ensure OnEnd fires exactly once.
fsmgo is licensed under the MIT License.
