State Machines in Go
Go's type system with generics (Go 1.18+) and interface-based polymorphism provides a clean foundation for state machine implementations. However, hand-writing complex state machines with hierarchical states, parallel regions, and history states requires significant boilerplate that is difficult to maintain as complexity grows.
Existing approaches compared
| Approach | Pros | Cons |
|---|---|---|
| switch/case | Simple, native Go | Manual, no hierarchy, no parallel states |
| looplab/fsm | Popular, callback-based | String-typed states, no hierarchy, runtime only |
| qmuntal/stateless | Fluent API, guards | No parallel states, no standard compliance |
| SCE | W3C standard, full hierarchy, AOT generated, generics | SCXML definition required, external code generation step |
SCE takes a unique approach in the Go ecosystem: instead of a runtime-configured DSL, you define state machines in W3C SCXML (the international standard for statecharts), and SCE's code generator produces idiomatic Go code at build time. This gives you the full power of the W3C specification -- hierarchical states, parallel regions, history, invoke, delayed events -- with type-safe generated code using Go generics.
How It Works
Step 1: Define your state machine in SCXML
<?xml version="1.0" encoding="UTF-8"?>
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0"
name="traffic_light" initial="red">
<state id="red">
<transition event="timer" target="green"/>
</state>
<state id="green">
<transition event="timer" target="yellow"/>
</state>
<state id="yellow">
<transition event="timer" target="red"/>
</state>
</scxml>
Step 2: Generate Go code
# Generate Go state machine (-l go or --language go)
sce-codegen generate --scxml traffic_light.scxml --language go --output-dir ./generated/
The generator produces a Go file containing a State type
(iota constants), an Event type, and a Policy struct
that implements the StatePolicy[S, E] interface. The
Engine[S, E] generic type from sce-go-runtime
drives execution.
Step 3: Use in your Go code
package main
import (
sce "github.com/newmassrael/sce-go-runtime"
gen "your-module/generated"
)
func main() {
// Create the policy (generated from SCXML) and engine
policy := gen.NewTrafficLightPolicy()
engine := sce.NewEngine[gen.TrafficLightState, gen.TrafficLightEvent](policy)
engine.Initialize() // Enters "red" state
engine.ProcessEvent(gen.TrafficLightEventTimer) // red -> green
engine.ProcessEvent(gen.TrafficLightEventTimer) // green -> yellow
engine.ProcessEvent(gen.TrafficLightEventTimer) // yellow -> red
fmt.Println(engine.GetCurrentState() == gen.TrafficLightStateRed) // true
}
ProcessEvent() is a convenience method that combines
RaiseExternal() + Step(). For fine-grained control,
use them separately:
// Low-level API: queue event, then process explicitly
engine.RaiseExternal(gen.TrafficLightEventTimer, "", "")
engine.Step()
W3C SCXML Features in Go
SCE's Go backend passes all 202 mandatory W3C SCXML conformance tests, supporting the full specification:
| Feature | W3C Section | Status |
|---|---|---|
| Compound (hierarchical) states | 3.3 | Supported |
| Parallel states | 3.4 | Supported |
| History states (shallow + deep) | 3.10 | Supported |
| ECMAScript datamodel | B.2 | Supported |
| Invoke (parent-child) | 6.4 | Supported |
| Delayed send | 6.2 | Supported |
Architecture
The Go backend consists of three modules with clear separation of concerns:
| Module | Purpose | Dependencies |
|---|---|---|
sce-go-runtime |
StatePolicy[S, E] interface, Engine[S, E] generic engine |
Go stdlib only |
sce-go-lua |
Lua 5.4 script engine via Shopify/go-lua | sce-go-runtime, go-lua |
sce-go-tests |
W3C SCXML conformance test suite (202 tests) | sce-go-runtime, sce-go-lua |
The runtime module has zero external dependencies -- it uses only the Go standard library. The Lua engine uses Shopify/go-lua, a pure Go implementation of Lua that requires no C compiler or CGo. This makes the entire stack cross-compilable to any platform Go supports.
Why SCXML for Go?
- Cross-language portability -- the same SCXML file generates C++, Kotlin, Rust, and Go code. Useful for projects with multi-language codebases or microservice architectures.
- Visual tooling -- SCXML can be visualized, simulated, and validated with standard tools. SCE includes a browser-based visualizer.
- Formal specification -- W3C SCXML defines precise semantics for state entry/exit order, event priority, conflict resolution, and parallel state synchronization. No ambiguity.
-
Type-safe generics -- generated code uses Go 1.22+
generics with
comparableconstraints, enabling compile-time type checking for state and event types. - No CGo -- the entire stack is pure Go, making it easy to cross-compile and deploy anywhere Go runs.
Comparison with Go State Machine Libraries
| Feature | switch/case | looplab/fsm | qmuntal/stateless | SCE |
|---|---|---|---|---|
| Hierarchical states | Manual | No | Yes (substates) | Yes |
| Parallel states | Manual | No | No | Yes |
| History states | Manual | No | No | Yes |
| Guarded transitions | Manual | No | Yes | Yes |
| Delayed events | Manual | No | No | Yes |
| Standard compliance | None | None | None | W3C SCXML 1.0 |
| Type safety | Manual | String-typed | Interface-typed | Generic (comparable) |
| Cross-language | No | No | No | C++, Kotlin, Rust, Go |
| Visual tooling | No | No | No | Browser visualizer |
Getting Started
# Clone the repository
git clone --recursive https://github.com/newmassrael/scxml-core-engine.git
cd scxml-core-engine
# Generate Go state machine from SCXML
sce-codegen generate --scxml your_machine.scxml --language go --output-dir ./generated/
# Run the Go W3C test suite
cd sce-go-tests && go test ./...
The Go runtime (sce-go-runtime/) provides the
Engine[S, E] type and StatePolicy[S, E] interface.
Test infrastructure is in sce-go-tests/. The code generator
produces standalone Go files that depend only on the runtime module.