State Pattern
Part of the Object Oriented Programming section of Coddy's GO journey — lesson 95 of 107.
The State pattern allows an object to change its behavior when its internal state changes, making it appear as if the object changed its class. While Template Method controls algorithm steps, State encapsulates state-specific behavior into separate objects and delegates to the current state.
In Go, we define a state interface and create concrete states that implement different behaviors:
type State interface {
Handle(d *Document) string
}
type Document struct {
state State
}
func (d *Document) SetState(s State) {
d.state = s
}
func (d *Document) Publish() string {
return d.state.Handle(d)
}Each state determines what happens and which state comes next:
type DraftState struct{}
func (s DraftState) Handle(d *Document) string {
d.SetState(ModerationState{})
return "Draft submitted for moderation"
}
type ModerationState struct{}
func (s ModerationState) Handle(d *Document) string {
d.SetState(PublishedState{})
return "Moderation approved, now published"
}
type PublishedState struct{}
func (s PublishedState) Handle(d *Document) string {
return "Already published"
}The same method call produces different results based on the current state:
doc := &Document{state: DraftState{}}
fmt.Println(doc.Publish()) // Draft submitted for moderation
fmt.Println(doc.Publish()) // Moderation approved, now published
fmt.Println(doc.Publish()) // Already publishedState is ideal for objects with distinct modes of operation, such as order processing workflows, UI components, or connection handlers where behavior depends entirely on the current state.
Challenge
EasyLet's build a ticket support system using the State pattern! You'll create a support ticket that moves through different stages—from being opened, to being worked on, to being resolved—with each state determining what actions are possible and what happens next.
You'll organize your code across three files:
state.go: Define your state interface and the concrete states that represent each stage of a ticket's lifecycle.Create a
TicketStateinterface with a methodHandle(t *Ticket) stringthat processes the ticket and potentially transitions it to the next state.Implement three states:
OpenState— when handled, transitions the ticket toInProgressStateand returnsTicket opened, assigning to support teamInProgressState— when handled, transitions toResolvedStateand returnsWorking on ticket, issue resolvedResolvedState— when handled, stays in the same state and returnsTicket already resolved
ticket.go: Create your ticket struct that holds the current state and delegates behavior to it.Build a
Ticketstruct with anIDfield (string) and astatefield (TicketState). Add these methods:SetState(s TicketState)to change the ticket's current stateProcess() stringthat delegates to the current state's Handle method
Create a
NewTicket(id string) *Ticketconstructor that returns a ticket starting in theOpenState.main.go: Demonstrate how the same action produces different results based on the ticket's current state.Read a ticket ID and the number of times to process the ticket. Create a new ticket with that ID, then call
Process()the specified number of times, printing each result on a separate line.
The following inputs will be provided:
- Line 1: Ticket ID
- Line 2: Number of times to process the ticket
For example, given:
TKT-001
3Your output should be:
Ticket opened, assigning to support team
Working on ticket, issue resolved
Ticket already resolvedAnd given:
TKT-500
5Your output should be:
Ticket opened, assigning to support team
Working on ticket, issue resolved
Ticket already resolved
Ticket already resolved
Ticket already resolvedAnd given:
ISSUE-42
1Your output should be:
Ticket opened, assigning to support teamNotice how calling Process() on the same ticket produces different results each time—the ticket's behavior changes as it moves through states. Once resolved, it stays resolved no matter how many times you process it. The ticket object appears to change its behavior, but it's actually delegating to different state objects!
Cheat sheet
The State pattern allows an object to change its behavior when its internal state changes. The object delegates behavior to separate state objects representing different states.
Define a state interface:
type State interface {
Handle(d *Document) string
}Create a context object that holds the current state:
type Document struct {
state State
}
func (d *Document) SetState(s State) {
d.state = s
}
func (d *Document) Publish() string {
return d.state.Handle(d)
}Implement concrete states that encapsulate state-specific behavior:
type DraftState struct{}
func (s DraftState) Handle(d *Document) string {
d.SetState(ModerationState{})
return "Draft submitted for moderation"
}
type ModerationState struct{}
func (s ModerationState) Handle(d *Document) string {
d.SetState(PublishedState{})
return "Moderation approved, now published"
}
type PublishedState struct{}
func (s PublishedState) Handle(d *Document) string {
return "Already published"
}Each state determines what happens and transitions to the next state:
doc := &Document{state: DraftState{}}
fmt.Println(doc.Publish()) // Draft submitted for moderation
fmt.Println(doc.Publish()) // Moderation approved, now published
fmt.Println(doc.Publish()) // Already publishedThe State pattern is ideal for objects with distinct modes of operation where behavior depends entirely on the current state.
Try it yourself
package main
import (
"fmt"
)
func main() {
// Read input
var ticketID string
var numProcesses int
fmt.Scanln(&ticketID)
fmt.Scanln(&numProcesses)
// TODO: Create a new ticket with the given ID
// TODO: Process the ticket the specified number of times
// and print each result on a separate line
}
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in Object Oriented Programming
1Fundamentals of Go OOP
External FilesGo Workspace & ModulesPackages & ImportsExported vs Unexported NamesIntroduction to OOP in GoStructs as ClassesDefining Methods on StructsPointer vs Value ReceiversStruct InitializationConstructor FunctionsRecap - Simple Calculator4Interfaces
Introduction to InterfacesImplicit ImplementationInterface as ContractEmpty Interface (any)Type AssertionType SwitchInterface CompositionStringer & Error InterfacesRecap - Shape Calculator7Encapsulation
Exported vs Unexported FieldsPackage-Level EncapsulationGetter & Setter MethodsInformation Hiding in GoRecap - Student Records10Generics (Go 1.18+)
Introduction to GenericsType ParametersType ConstraintsGeneric StructsGeneric Methods WorkaroundRecap - Generic Collection2Types & Structs Deep Dive
Basic & Composite TypesCustom Type DefinitionsStruct TagsAnonymous StructsNested StructsZero Values & DefaultsRecap - Contact Book5Composition Over Inheritance
Why Go Has No InheritanceStruct Embedding BasicsMethod PromotionEmbedding Multiple StructsEmbedding vs AggregationShadowing Embedded MethodsRecap - Employee Hierarchy8Error Handling & OOP
The error InterfaceCustom Error TypesError Wrapping (fmt.Errorf)Sentinel Errorserrors.Is() and errors.As()Panic, Defer, and RecoverRecap - File Parser11Standard Library & OOP
io.Reader & io.Writersort.Interfacefmt.Stringer Interfaceencoding/json with Structshttp.Handler InterfaceRecap - REST API Models14Design Patterns Part 2
Command PatternAdapter PatternDecorator PatternTemplate Method PatternState PatternComposite PatternMiddleware as Decorator3Pointers & Memory
Pointer Basics in GoPointers to StructsPass by Value vs ReferenceThe new() FunctionGarbage Collection in GoRecap - Linked List Builder6Polymorphism in Go
Polymorphism via InterfacesDuck Typing in GoInterface Satisfaction RulesPolymorphic CollectionsDependency InjectionRecap - Payment Processor9Concurrency & OOP
Goroutines BasicsChannels & CommunicationBuffered vs Unbuffered ChanSelect Statementsync.Mutex & sync.RWMutexsync.WaitGroupThread-Safe Struct DesignRecap - Worker Pool