{} The Go Reference

Behavioral pattern · Gang of Four · Intermediate

State

Let an object alter its behavior when its internal state changes, as if it changed class.

Behavioral Intermediate ⏱ 4 min read Complete

🚦 Analogy

A turnstile is either locked or unlocked, and the same actions mean different things depending on which. Insert a coin while locked and it unlocks; push while unlocked and you go through — and it locks again. The object’s behavior changes with its state, as if it briefly became a different machine.

The problem

When behavior depends on state, the naive code sprouts the same switch state { … } in every method, and adding a state means editing them all. The State pattern makes each state its own type implementing a shared interface. The context delegates to its current state, and each state knows how to behave and what to transition to.

Structure

classDiagram
class Turnstile {
  -state State
  +Coin()
  +Push()
}
class State {
  <<interface>>
  +Coin(t)
  +Push(t)
}
class Locked { +Coin() +Push() }
class Unlocked { +Coin() +Push() }
Turnstile o--> State : current
State <|.. Locked
State <|.. Unlocked

How it works

stateDiagram-v2
[*] --> Locked
Locked --> Unlocked : Coin
Unlocked --> Locked : Push
Locked --> Locked : Push (blocked)
Unlocked --> Unlocked : Coin (returned)

Idiomatic Go

Each state is a small struct implementing State; transitions just reassign the turnstile’s state field. Edit and Run:

state.go — editable & runnable
package main

import "fmt"

// State defines how the turnstile reacts to each event.
type State interface {
Coin(t *Turnstile)
Push(t *Turnstile)
}

type Turnstile struct{ state State }

func (t *Turnstile) Coin() { t.state.Coin(t) }
func (t *Turnstile) Push() { t.state.Push(t) }

type Locked struct{}

func (Locked) Coin(t *Turnstile) {
fmt.Println("coin accepted → unlocking")
t.state = Unlocked{} // transition
}
func (Locked) Push(t *Turnstile) { fmt.Println("push blocked (locked)") }

type Unlocked struct{}

func (Unlocked) Coin(t *Turnstile) { fmt.Println("already unlocked, coin returned") }
func (Unlocked) Push(t *Turnstile) {
fmt.Println("push → you go through, locking")
t.state = Locked{} // transition
}

func main() {
t := &Turnstile{state: Locked{}}
t.Push() // blocked
t.Coin() // unlock
t.Push() // go through, lock
t.Push() // blocked again
}

🐹 State vs Strategy — who's in charge

Structurally they’re twins: a context delegating to an interface. The difference is agency. A Strategy is handed to the object from outside and just runs. A State drives the machine — it decides when to flip the context to the next state. In Go each state is usually a tiny (often empty) struct, so the whole machine is just a handful of methods and a state field, replacing a pile of conditionals.

The function-based state machine

Go’s standard library rarely uses a struct-per-state. Its lexers (text/template, go/scanner) use Rob Pike’s stateFn style instead: a state is a function that does some work and returns the next state function. The driver is a one-line loop, and there’s no interface or context struct at all:

statefn.go — editable & runnable
package main

import "fmt"

// A state is a function that returns the next state (or nil to stop).
type stateFn func(*machine) stateFn

type machine struct {
events []string
i      int
log    []string
}

func (m *machine) next() (string, bool) {
if m.i >= len(m.events) {
	return "", false
}
e := m.events[m.i]
m.i++
return e, true
}

func locked(m *machine) stateFn {
e, ok := m.next()
if !ok {
	return nil
}
if e == "coin" {
	m.log = append(m.log, "unlock")
	return unlocked
}
m.log = append(m.log, "blocked")
return locked
}

func unlocked(m *machine) stateFn {
e, ok := m.next()
if !ok {
	return nil
}
if e == "push" {
	m.log = append(m.log, "go+lock")
	return locked
}
m.log = append(m.log, "coin returned")
return unlocked
}

func main() {
m := &machine{events: []string{"push", "coin", "push", "push"}}
for state := stateFn(locked); state != nil; {
	state = state(m) // each state names its successor
}
fmt.Println(m.log)
}

Same state machine as the turnstile above, no types — just functions returning functions. Reach for the struct form when states carry data; reach for stateFn for lexers/parsers and tight event loops.

In practice

State machines are everywhere: TCP connection states, HTTP/2 streams, order/payment lifecycles, parser/lexer states, and game logic.

Pitfalls

⚠️ Don't make a state machine out of a boolean

If you have two states and one transition, a bool and an if are clearer than two structs and an interface. The pattern pays off when states multiply, each carries its own behavior, and the transition table would otherwise be scattered across many switch statements.

When to use it — and when not

✅ Reach for it when

  • An object's behavior depends on its state, and you have big switch/if blocks on a state field repeated across methods.
  • There are several states with their own behavior and clear transitions between them.
  • States should decide their own next state.

⛔ Think twice when

  • There are only two trivial states — a boolean and a switch is simpler.
  • Transitions are so simple they don't justify a type per state.

Check your understanding

Score: 0 / 5

1. How does State differ from Strategy?

Both swap behavior behind an interface, but a state machine moves itself between states, whereas a strategy is selected by the client and stays put.

2. What does the State pattern replace?

Each state becomes its own type implementing a shared interface; the context delegates to the current state, so the conditionals disappear.

3. Where does the transition logic live?

A concrete state handles an event and assigns the context's `state` field to the next state, keeping each transition local to where it makes sense.

4. What is the idiomatic 'function-based' state machine in Go (Rob Pike's lexer style)?

Instead of a type per state, each state is a function returning the next state function; the driver loops `state = state(m)` until it returns nil. text/template and go/scanner use this exact shape — it's the most Go-native State machine.

5. When does a struct-per-state (or stateFn) beat a plain switch on a state field?

For two states and one transition, a bool + if wins. The pattern pays off when behavior-per-state grows and the same switch(state) would otherwise be duplicated across every method — then localizing each state's behavior and transition into its own type/function removes the scatter.

Comments

Sign in with GitHub to join the discussion.