Skip to content

enetx/fsm

Repository files navigation

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.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published