fsm

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 28, 2026 License: MIT Imports: 5 Imported by: 5

README

image

FSM for Go

A generic, concurrent-safe, and easy-to-use finite state machine (FSM) library for Go.

This library provides a simple yet powerful API for defining states and transitions, handling callbacks, and managing stateful logic in your applications. It is built with types and utilities from the github.com/enetx/g library.

Go Reference Go Report Card Coverage Status Go Ask DeepWiki

Features

  • Simple & Fluent API: Define your state machine with clear, chainable methods.
  • Fast by Default: The base FSM is non-blocking for maximum performance in single-threaded use cases.
  • Drop-in Concurrency: Get a fully thread-safe FSM by calling a single Sync() method.
  • State Callbacks: Execute code on entering (OnEnter) or exiting (OnExit) a state.
  • Global Transition Hooks: OnTransition allows you to monitor and log all state changes globally.
  • Guarded Transitions: Control transitions with TransitionWhen based on custom logic.
  • JSON Serialization: Easily save and restore the FSM's state with built-in json.Marshaler and json.Unmarshaler support.
  • Graphviz Visualization: Generate DOT-format graphs to visualize your FSM.
  • Zero Dependencies (besides github.com/enetx/g).

Installation

go get github.com/enetx/fsm

Quick Start

Here's a simple example of a traffic light state machine:

package main

import (
	"fmt"
	"time"

	"github.com/enetx/fsm"
)

func main() {
	// 1. Define states and the event
	const (
		StateGreen  = "Green"
		StateYellow = "Yellow"
		StateRed    = "Red"
		EventTimer  = "timer_expires"
	)

	// 2. Configure the FSM
	lightFSM := fsm.New(StateRed).
		Transition(StateGreen, EventTimer, StateYellow).
		Transition(StateYellow, EventTimer, StateRed).
		Transition(StateRed, EventTimer, StateGreen)

	// 3. Define callbacks for entering states
	lightFSM.OnEnter(StateGreen, func(ctx *fsm.Context) error {
		fmt.Println("LIGHT: Green -> Go!")
		return nil
	})
	lightFSM.OnEnter(StateYellow, func(ctx *fsm.Context) error {
		fmt.Println("LIGHT: Yellow -> Prepare to stop")
		return nil
	})
	lightFSM.OnEnter(StateRed, func(ctx *fsm.Context) error {
		fmt.Println("LIGHT: Red -> Stop!")
		return nil
	})

	// 4. Run the FSM loop
	fmt.Printf("Initial state: %s\n", lightFSM.Current())
	lightFSM.CallEnter(StateRed) // Manually trigger the first prompt

	for range 4  {
		time.Sleep(1 * time.Second)
		fmt.Println("\n...timer expires...")
		lightFSM.Trigger(EventTimer)
	}
}
Output
Initial state: Red
LIGHT: Red -> Stop!

...timer expires...
LIGHT: Green -> Go!

...timer expires...
LIGHT: Yellow -> Prepare to stop

...timer expires...
LIGHT: Red -> Stop!

...timer expires...
LIGHT: Green -> Go!

API Overview

Creating an FSM
// Create a new FSM instance (not thread-safe)
fsmachine := fsm.New("initial_state")

// Get a thread-safe wrapper for concurrent use
safeFSM := fsmachine.Sync()
Defining Transitions
  • Transition(from, event, to): A direct, unconditional transition.
  • TransitionWhen(from, event, to, guard): A transition that only occurs if the guard function returns true.
fsmachine.Transition("idle", "start", "running")

fsmachine.TransitionWhen("running", "stop", "stopped", func(ctx *fsm.Context) bool {
    // Only allow stopping if a specific condition is met
    return ctx.Data.Get("can_stop").UnwrapOr(false).(bool)
})
Callbacks and Hooks
  • OnEnter(state, callback): Called when the FSM enters state.
  • OnExit(state, callback): Called before the FSM exits state.
  • OnTransition(hook): Called on every successful transition, after OnExit and before OnEnter.
fsmachine.OnEnter("running", func(ctx *fsm.Context) error {
    fmt.Println("Job started!")
    return nil
})

fsmachine.OnExit("running", func(ctx *fsm.Context) error {
    fmt.Println("Cleaning up job...")
    return nil
})

fsmachine.OnTransition(func(from, to fsm.State, event fsm.Event, ctx *fsm.Context) error {
    log.Printf("STATE CHANGE: %s -> %s (on event %s)", from, to, event)
    return nil
})
Triggering Events

The Trigger method drives the state machine.

// Simple trigger
err := fsmachine.Trigger("start")

// Trigger with data payload
// The data will be available in the context as `ctx.Input`.
err := fsmachine.Trigger("process", someDataObject)

Any error returned from a callback will halt the transition and be returned by Trigger.

Context

The Context is passed to every callback and guard. It's the primary way to manage data associated with an FSM instance.

  • ctx.Input: Holds the data passed with the current Trigger call. It's ephemeral and lasts for one transition only.
  • ctx.Data: A concurrent-safe map (g.MapSafe) for persistent data that is serialized with the FSM (e.g., user details).
  • ctx.Meta: A concurrent-safe map (g.MapSafe) for ephemeral metadata that is also serialized (e.g., temporary counters).
Concurrency

The library is designed with performance and safety in mind, offering two distinct operating modes:

  1. fsm.FSM (Default): The base state machine is not thread-safe. It is optimized for performance in single-threaded scenarios by avoiding the overhead of mutexes.

  2. fsm.SyncFSM (Synchronized): This is a thread-safe wrapper around the base FSM. It protects all operations (like Trigger, Current, Reset) with a mutex, ensuring that all transitions are atomic and safe to use across multiple goroutines.

You should complete all configuration (Transition, OnEnter, etc.) on the base FSM before using it. The configuration process itself is not thread-safe.

Activating Thread-Safety

To get a thread-safe instance, simply call the Sync() method after you have configured your FSM:

// 1. Configure the non-thread-safe FSM template
fsmTemplate := fsm.New("idle").
    Transition("idle", "start", "running").
    Transition("running", "stop", "stopped")

// 2. Get a thread-safe, synchronized instance
safeFSM := fsmTemplate.Sync()

// 3. Now you can safely use safeFSM across multiple goroutines
go func() {
    err := safeFSM.Trigger("start")
    // ...
}()

go func() {
    currentState := safeFSM.Current()
    // ...
}()
Serialization

You can easily save and restore the FSM's state using encoding/json, as FSM implements the json.Marshaler and json.Unmarshaler interfaces.

Saving State:

// Assume `fsmachine` is in some state.
jsonData, err := json.Marshal(fsmachine)
if err != nil {
    // handle error
}
// Now you can save `jsonData` to a database, file, etc.

Restoring State:

// 1. Create a new FSM with the same configuration as the original.
restoredFSM := fsm.New("initial_state").
    Transition(...) // ...add all transitions and callbacks

// 2. Unmarshal the JSON data into the new instance.
err := json.Unmarshal(jsonData, restoredFSM)
if err != nil {
    // handle error
}

// `restoredFSM` is now in the same state as the original was.
fmt.Println(restoredFSM.Current())

Note: Serialization only saves the FSM's state (current, history, Data, Meta). It does not save the transition rules or callbacks. You must configure the FSM template before unmarshaling. If you need a thread-safe FSM after restoring, call .Sync() after json.Unmarshal.

Visual Generator (Web UI)

An in-browser FSM editor and Go code generator for this library.

Open the Online Generator →

  • 100% client-side (no data sent anywhere).
  • Draw states and transitions, set callbacks and guards, then generate ready-to-use Go code for github.com/enetx/fsm.
Controls
  • Double-click empty canvas — add a state.
  • Double-click state/transition — rename state / edit event name.
  • Shift + drag from one state to another — create a transition (self-loops supported).
  • Right-click state — context menu (Set as Initial / Delete).
  • Drag on empty canvas — rectangular multi-select; then use Align X, Align Y, Stack.
  • Esc — cancel linking / clear selection.
Properties & Panels
  • State properties: name, color, OnEnter, OnExit, “Final state”, and “Set as Initial”.
  • Transition properties: event name and optional guard function.
  • Events panel: shows incoming/outgoing events for the selected state (guards are italicized).
Generate Go Code

Click “Generate Go Code” to get a self-contained example:

  • Declares const States and Events.
  • Builds an FSM via fsm.New(initial) with .Transition(...) / .TransitionWhen(..., guard).
  • Attaches callbacks with .OnEnter(...) / .OnExit(...).
  • Emits function stubs for every referenced callback/guard (once per unique name).

Note: You must set an initial state before generating code. Callback/guard names you type in the UI become function names in the output.

Import / Export
  • Export JSON — downloads fsm.json with positions, colors, callbacks, guards, transitions, and initial state.
  • Import JSON — loads a saved model. If positions are missing, the tool auto-lays out nodes.
Validation & Hints
  • State names must be unique (enforced by the editor).
  • Warns about unreachable states.
  • Guarded transitions are rendered with dashed lines and a diamond arrowhead.
Visualization

The library includes a ToDOT() method to generate a graph of your state machine in the DOT language. This is extremely useful for debugging, documentation, and sharing your FSM's logic with your team.

You can render the output into an image using various tools:

  • Online Editors (Recommended for quick use):

    • Graphviz Online - A simple and effective web-based viewer.
    • Edotor - Another powerful online editor with different layout engines.
    • Simply paste the output of ToDOT() into one of these sites to see your diagram instantly.
  • Local Installation:

    • For more advanced use or integration into build scripts, you can install Graphviz locally.

Example:

func main() {
    fsmachine := fsm.New("Idle").
        Transition("Idle", "start", "Running").
        TransitionWhen("Running", "suspend", "Suspended", func(ctx *fsm.Context) bool {
            return true
        }).
        Transition("Suspended", "resume", "Running").
        Transition("Running", "finish", "Done")

    // Generate the DOT string
    fsmachine.ToDOT().Println() // Copy this output
}
graphviz

Contributing

Contributions are welcome! Please feel free to submit a pull request or open an issue for bugs, feature requests, or questions.

License

This project is licensed under the MIT License. See the LICENSE file for details.

Documentation

Overview

Package fsm provides a generic finite state machine (FSM) implementation with support for transitions, guards, and enter/exit callbacks. It is built with types and utilities from the github.com/enetx/g library. The base FSM is NOT concurrent-safe. For concurrent use, wrap it using the .Sync() method.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Callback

type Callback func(ctx *Context) error

Callback is a function called on entering or exiting a state.

type Context

type Context struct {
	State State
	Input any
	Data  *g.MapSafe[g.String, any]
	Meta  *g.MapSafe[g.String, any]
}

Context holds FSM state, input, persistent and temporary data. Data is for long-lived values (e.g. user ID, settings) and is serialized. Meta is for ephemeral metadata (e.g. timestamps, counters) and is also serialized. Input holds data specific to the current trigger event and is NOT serialized. State holds the state for which a callback is being executed.

type ErrAmbiguousTransition

type ErrAmbiguousTransition struct {
	From  State
	Event Event
}

ErrAmbiguousTransition is returned when a trigger event results in more than one valid transition. This typically happens due to a configuration error where multiple guards for the same event from the same state return true. The FSM's behavior is ambiguous, so the transition is aborted to prevent non-deterministic behavior.

func (*ErrAmbiguousTransition) Error

func (e *ErrAmbiguousTransition) Error() string

type ErrCallback

type ErrCallback struct {
	// HookType is the type of callback or hook where the error occurred (e.g., "OnEnter", "OnTransition").
	HookType string
	// State is the state associated with the callback. It may be empty for global hooks.
	State State
	// Err is the original error returned by the callback or the error created after recovering from a panic.
	Err error
}

ErrCallback is returned when a callback (OnEnter, OnExit) or a hook (OnTransition) returns an error or panics. It wraps the original error, allowing it to be inspected using functions like errors.Is and errors.As.

func (*ErrCallback) Error

func (e *ErrCallback) Error() string

func (*ErrCallback) Unwrap

func (e *ErrCallback) Unwrap() error

Unwrap provides compatibility with the standard library's errors package, allowing the use of errors.Is and errors.As to inspect the wrapped error.

type ErrInvalidTransition

type ErrInvalidTransition struct {
	From  State
	Event Event
}

ErrInvalidTransition is returned when no matching transition is found for the given event from the current state.

func (*ErrInvalidTransition) Error

func (e *ErrInvalidTransition) Error() string

type ErrUnknownState

type ErrUnknownState struct {
	State State
}

ErrUnknownState is returned when attempting to unmarshal a state that has not been defined in the FSM's configuration. This prevents the FSM from entering an invalid, undeclared state.

func (*ErrUnknownState) Error

func (e *ErrUnknownState) Error() string

type Event

type Event g.String

Event represents an event that triggers a transition.

type FSM

type FSM struct {
	// contains filtered or unexported fields
}

FSM is the main state machine struct.

func New

func New(initial State) *FSM

New creates a new FSM with the given initial state.

func (*FSM) CallEnter

func (f *FSM) CallEnter(state State) error

CallEnter manually invokes all OnEnter callbacks for a state without a transition.

func (*FSM) Clone

func (f *FSM) Clone() *FSM

Clone creates a new FSM instance with the same configuration but a fresh state.

func (*FSM) Context

func (f *FSM) Context() *Context

Context returns the FSM's context for managing data.

func (*FSM) Current

func (f *FSM) Current() State

Current returns the FSM's current state.

func (*FSM) History

func (f *FSM) History() g.Slice[State]

History returns a copy of the list of previously visited states.

func (*FSM) MarshalJSON

func (f *FSM) MarshalJSON() ([]byte, error)

MarshalJSON implements the json.Marshaler interface.

func (*FSM) OnEnter

func (f *FSM) OnEnter(state State, cb Callback) *FSM

OnEnter registers a callback for when entering a given state.

func (*FSM) OnExit

func (f *FSM) OnExit(state State, cb Callback) *FSM

OnExit registers a callback for when exiting a given state.

func (*FSM) OnTransition

func (f *FSM) OnTransition(hook TransitionHook) *FSM

OnTransition registers a global transition hook.

func (*FSM) Reset

func (f *FSM) Reset()

Reset resets the FSM to its initial state and clears all context data.

func (*FSM) SetState

func (f *FSM) SetState(s State)

SetState manually sets the current state, without triggering any callbacks or guards. WARNING: This is a low-level method that bypasses all FSM logic (OnExit, OnEnter callbacks, transition hooks, and guards). It does not update the state history. It should only be used for specific scenarios like restoring the FSM from storage or for manual administrative intervention. For all standard operations, use Trigger.

func (*FSM) States

func (f *FSM) States() g.Slice[State]

States returns a slice of all unique states defined in the FSM's transitions.

func (*FSM) Sync

func (f *FSM) Sync() *SyncFSM

Sync wraps the FSM in a concurrent-safe shell.

func (*FSM) ToDOT

func (f *FSM) ToDOT() g.String

ToDOT generates a DOT language string representation of the FSM for visualization.

func (*FSM) Transition

func (f *FSM) Transition(from State, event Event, to State) *FSM

Transition adds a basic transition (without a guard) from -> event -> to.

func (*FSM) TransitionWhen

func (f *FSM) TransitionWhen(from State, event Event, to State, guard GuardFunc) *FSM

TransitionWhen adds a guarded transition from -> event -> to.

func (*FSM) Trigger

func (f *FSM) Trigger(event Event, input ...any) error

Trigger attempts to transition using the given event. It accepts an optional single 'input' argument to pass data to guards and callbacks. This input is only valid for the duration of this specific trigger cycle.

func (*FSM) UnmarshalJSON

func (f *FSM) UnmarshalJSON(data []byte) error

UnmarshalJSON implements the json.Unmarshaler interface.

type FSMState

type FSMState struct {
	Current State                `json:"current"`
	History g.Slice[State]       `json:"history"`
	Data    g.Map[g.String, any] `json:"data"`
	Meta    g.Map[g.String, any] `json:"meta"`
}

FSMState is a serializable representation of the FSM's state. It uses standard map types for robust JSON handling.

type GuardFunc

type GuardFunc func(ctx *Context) bool

GuardFunc determines whether a transition is allowed.

type State

type State g.String

State represents a finite state in the FSM.

type StateMachine

type StateMachine interface {
	Trigger(Event, ...any) error
	Current() State
	Context() *Context
	SetState(State)
	Reset()
	History() g.Slice[State]
	States() g.Slice[State]
	ToDOT() g.String
	MarshalJSON() ([]byte, error)
	UnmarshalJSON(data []byte) error
}

type SyncFSM

type SyncFSM struct {
	// contains filtered or unexported fields
}

SyncFSM is a thread-safe wrapper around an FSM. It protects all state-mutating and state-reading operations with a sync.RWMutex, making it safe for use across multiple goroutines. All methods on SyncFSM are the thread-safe counterparts to the methods on the base FSM.

func (*SyncFSM) CallEnter

func (sf *SyncFSM) CallEnter(state State) error

CallEnter is the thread-safe version of FSM.CallEnter. It manually invokes the OnEnter callbacks for a given state without a transition.

func (*SyncFSM) Context

func (sf *SyncFSM) Context() *Context

Context is the thread-safe version of FSM.Context. It returns a pointer to the FSM's context.

func (*SyncFSM) Current

func (sf *SyncFSM) Current() State

Current is the thread-safe version of FSM.Current. It returns the FSM's current state.

func (*SyncFSM) History

func (sf *SyncFSM) History() g.Slice[State]

History is the thread-safe version of FSM.History. It returns a copy of the state transition history.

func (*SyncFSM) MarshalJSON

func (sf *SyncFSM) MarshalJSON() ([]byte, error)

MarshalJSON implements the json.Marshaler interface for thread-safe serialization of the FSM's state to JSON.

func (*SyncFSM) Reset

func (sf *SyncFSM) Reset()

Reset is the thread-safe version of FSM.Reset. It resets the FSM to its initial state and clears its context.

func (*SyncFSM) SetState

func (sf *SyncFSM) SetState(s State)

SetState is the thread-safe version of FSM.SetState. It forcefully sets the current state, bypassing all callbacks and guards. WARNING: This is a low-level method intended for specific use cases like state restoration. For all standard operations, use Trigger.

func (*SyncFSM) States

func (sf *SyncFSM) States() g.Slice[State]

States is the thread-safe version of FSM.States. It returns a slice of all unique states defined in the FSM.

func (*SyncFSM) ToDOT

func (sf *SyncFSM) ToDOT() g.String

ToDOT is the thread-safe version of FSM.ToDOT. It generates a DOT language string representation of the FSM for visualization.

func (*SyncFSM) Trigger

func (sf *SyncFSM) Trigger(event Event, input ...any) error

Trigger is the thread-safe version of FSM.Trigger. It atomically executes a state transition in response to an event.

func (*SyncFSM) UnmarshalJSON

func (sf *SyncFSM) UnmarshalJSON(data []byte) error

UnmarshalJSON implements the json.Unmarshaler interface for thread-safe deserialization of the FSM's state from JSON.

type TransitionHook

type TransitionHook func(from, to State, event Event, ctx *Context) error

TransitionHook is a global callback called after a transition between states. It runs after OnExit and before OnEnter.

Directories

Path Synopsis
battle command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL