efsm is a generic, thread-safe, extended finite state machine (EFSM) for Go. It provides a fluent builder API to define states, events, and transition guards, making it easy to model complex logic safely in concurrent environments.
It is relentlessly optimized for high-throughput, highly concurrent environments. It features a zero-allocation pointer-graph architecture, 100% lock-free reads, and CPU cache-line padding to prevent false sharing.
- Zero-Allocation Hot Path: Transitioning states (
Fire) requires 0 heap allocations. - Type-Safe Generics: Define your own state and event types without empty interfaces (
interface{}orany). - Highly Concurrent: 100% lock-free reads (
CurrentState), mutually exclusive writes, and CPU cache-line padding to prevent false sharing. - Composable Mixins: Use the
ConfigureAPI to write reusable state logic (like telemetry or error handling) and apply it across multiple states. - Dynamic Routing: Resolve target states dynamically at runtime based on data context (
PermitRedirect). - Guards & Effects: Hook into state transitions with
WithGuard,OnEntry,OnExit, andOnTransition.
Requires Go 1.18 or later (uses Generics).
go get github.com/webermarci/efsmpackage main
import (
"errors"
"fmt"
"github.com/webermarci/efsm"
)
// 1. Define states and events as strongly typed aliases
type State string
type Event string
const (
StateDisconnected State = "Disconnected"
StateConnecting State = "Connecting"
StateConnected State = "Connected"
StateFailed State = "Failed"
EventConnect Event = "Connect"
EventDisconnect Event = "Disconnect"
EventSuccess Event = "Success"
EventError Event = "Error"
EventTimeout Event = "Timeout"
)
type Data struct {
RetryCount int
IPAddress string
}
func main() {
sm := efsm.NewStateMachine[State, Event, Data](StateDisconnected)
// --- 2. Define Reusable Mixins ---
// Mixins can define standard logging, telemetry, or shared transitions.
// Their hooks will execute alongside the state-specific hooks!
withTelemetry := func(c *efsm.StateConfigurator[State, Event, Data]) {
c.OnEntry(func(t efsm.Transition[State, Event], data Data) {
fmt.Printf("[Telemetry] Entered state: %s\n", t.To)
})
c.Permit(EventDisconnect, StateDisconnected, efsm.OnTransition(
func(t efsm.Transition[State, Event], data Data) {
fmt.Println("[Telemetry] Connection cleanly aborted.")
},
))
}
// --- 3. Configure States ---
sm.Configure(StateDisconnected)
sm.Configure(StateConnecting,
withTelemetry,
func(c *efsm.StateConfigurator[State, Event, Data]) {
// This OnEntry runs right after the telemetry OnEntry
c.OnEntry(func(t efsm.Transition[State, Event], data Data) {
fmt.Printf("⏳ Attempting connection to %s...\n", data.IPAddress)
})
c.Permit(EventSuccess, StateConnected, efsm.WithGuard(
func(t efsm.Transition[State, Event], data Data) error {
if data.IPAddress == "" {
return errors.New("missing IP address")
}
return nil
},
))
// Dynamic redirect based on runtime context
c.PermitRedirect(EventError, func(t efsm.Transition[State, Event], data Data) State {
if data.RetryCount >= 3 {
return StateFailed
}
return StateDisconnected
})
for _, event := range []Event{EventTimeout} {
c.Permit(event, StateFailed)
}
},
)
sm.Configure(StateConnected,
withTelemetry,
func(c *efsm.StateConfigurator[State, Event, Data]) {
c.OnEntry(func(t efsm.Transition[State, Event], d Data) {
fmt.Println("✅ Connection established successfully!")
})
},
)
sm.Configure(StateFailed, func(c *efsm.StateConfigurator[State, Event, Data]) {
c.Permit(EventConnect, StateConnecting)
})
// --- 4. Execute ---
payload := Data{RetryCount: 3, IPAddress: "192.168.1.100"}
// Use CanFire to check if an action is valid before trying it (great for UI rendering)
if sm.CanFire(EventConnect) {
fmt.Println("Button 'Connect' is enabled.")
}
// Use MustFire when you are programmatically certain the event is valid
// and want a panic on developer error instead of checking err != nil
sm.MustFire(EventConnect, payload)
// Normal Fire for events that might be rejected by a guard or state mismatch
err := sm.Fire(EventError, payload)
if err != nil {
fmt.Printf("Failed to fire: %v\n", err)
}
fmt.Printf("\nFinal State: %s\n", sm.CurrentState())
}To guarantee that state transitions are strictly atomic and ordered, efsm holds an internal lock during the execution of a transition and its associated effects (OnExit, OnTransition, OnEntry).
sm.Fire(), sm.CanFire(), or sm.MustFire() synchronously from within an effect or guard. Doing so will cause a deadlock.
If an entry effect needs to trigger a subsequent state transition, you must spawn a new goroutine to push the event to the back of the line:
sm.Configure(StateConnecting, func(c *efsm.StateConfigurator[State, Event, Data]) {
c.OnEntry(func(t efsm.Transition[State, Event], data Data) {
// Correct: Run async so the current transition can finish and release the lock
go func() {
err := connectToNetwork(data.IPAddress)
if err != nil {
sm.Fire(EventError, data)
} else {
sm.Fire(EventSuccess, data)
}
}()
})
})goos: darwin
goarch: arm64
pkg: github.com/webermarci/efsm
cpu: Apple M5
BenchmarkStateMachine_Fire-10 124017261 9.43 ns/op 0 B/op 0 allocs/op
BenchmarkStateMachine_FireEffects-10 121889860 9.86 ns/op 0 B/op 0 allocs/op
BenchmarkStateMachine_State_Parallel-10 1000000000 0.19 ns/op 0 B/op 0 allocs/op
BenchmarkStateMachine_Fire_Parallel-10 17264893 68.48 ns/op 0 B/op 0 allocs/op
BenchmarkStateMachine_FireRedirect-10 100000000 11.50 ns/op 0 B/op 0 allocs/op
BenchmarkStateMachine_FireRedirect_Parallel-10 17193207 69.46 ns/op 0 B/op 0 allocs/op