This series of articles looks at various patterns and how they apply to Go, starting with the GoF creational patterns.
Verdicts: Link to heading
- Idiomatic - Considered a best practice in Go.
- Code Smell - Not wrong, but if you are using it, make sure you understand what you are doing.
- Anti-Pattern - Avoid unless you are an expert and know it is okay to break this rule.
Chain of Responsibility Pattern Link to heading
Problem Space Link to heading
The Chain of Responsibility pattern passes a request along a chain of handlers. Each handler decides either to process the request or pass it to the next handler in the chain. This decouples the sender from the receiver and allows multiple handlers to process the request.
Practical Example 1 Link to heading
cmd/server/main.go
package main
import (
"log"
"net/http"
"time"
)
func logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
})
}
func recovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("panic recovered: %v", err)
http.Error(w, "internal server error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
func auth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return // chain stops here
}
next.ServeHTTP(w, r)
})
}
func hello(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello"))
}
func main() {
handler := http.HandlerFunc(hello)
// Build chain: recovery -> logging -> auth -> hello
chain := recovery(logging(auth(handler)))
http.Handle("/", chain)
http.ListenAndServe(":8080", nil)
}
Practical Example 2 Link to heading
internal/validation/validation.go
package validation
import "fmt"
type Request struct {
UserID int
Amount float64
Currency string
}
type Validator interface {
SetNext(Validator) Validator
Validate(Request) error
}
type baseValidator struct {
next Validator
}
func (v *baseValidator) SetNext(next Validator) Validator {
v.next = next
return next
}
func (v *baseValidator) passToNext(r Request) error {
if v.next != nil {
return v.next.Validate(r)
}
return nil
}
// User validation
type userValidator struct {
baseValidator
}
func NewUserValidator() *userValidator {
return &userValidator{}
}
func (v *userValidator) Validate(r Request) error {
if r.UserID <= 0 {
return fmt.Errorf("invalid user ID")
}
return v.passToNext(r)
}
// Amount validation
type amountValidator struct {
baseValidator
}
func NewAmountValidator() *amountValidator {
return &amountValidator{}
}
func (v *amountValidator) Validate(r Request) error {
if r.Amount <= 0 {
return fmt.Errorf("amount must be positive")
}
if r.Amount > 10000 {
return fmt.Errorf("amount exceeds limit")
}
return v.passToNext(r)
}
// Currency validation
type currencyValidator struct {
baseValidator
allowed map[string]bool
}
func NewCurrencyValidator(currencies []string) *currencyValidator {
allowed := make(map[string]bool)
for _, c := range currencies {
allowed[c] = true
}
return ¤cyValidator{allowed: allowed}
}
func (v *currencyValidator) Validate(r Request) error {
if !v.allowed[r.Currency] {
return fmt.Errorf("unsupported currency: %s", r.Currency)
}
return v.passToNext(r)
}
cmd/cli/main.go
package main
import (
"fmt"
"myapp/internal/validation"
)
func main() {
// Build validation chain
userVal := validation.NewUserValidator()
amountVal := validation.NewAmountValidator()
currencyVal := validation.NewCurrencyValidator([]string{"USD", "EUR", "GBP"})
userVal.SetNext(amountVal).SetNext(currencyVal)
// Valid request
req := validation.Request{
UserID: 42,
Amount: 100.00,
Currency: "USD",
}
if err := userVal.Validate(req); err != nil {
fmt.Println("validation failed:", err)
} else {
fmt.Println("validation passed")
}
// Invalid request
badReq := validation.Request{
UserID: 42,
Amount: 100.00,
Currency: "BTC",
}
if err := userVal.Validate(badReq); err != nil {
fmt.Println("validation failed:", err)
}
}
Considerations Link to heading
Example 1: HTTP middleware is Chain of Responsibility. Each handler wraps the next, and any handler can stop the chain by not calling the next handler. This is the most common pattern in Go web applications.
Example 2: The explicit chain with SetNext is closer to the GoF pattern. Each validator checks its concern and passes to the next. Useful when the chain needs to be built dynamically or reconfigured at runtime.
Example 1 is more idiomatic Go. The function wrapping approach is simpler and composes well. Example 2 is useful when you need the chain structure to be more explicit or modifiable.
Verdict Link to heading
- Example 1: Idiomatic
- Example 2: Code Smell (use when chain needs runtime configuration)
Command Pattern Link to heading
Problem Space Link to heading
The Command pattern encapsulates a request as an object, allowing you to parameterize functions with different requests, queue operations, log actions, or support undo functionality. In Go, functions are first-class citizens, so the pattern often simplifies to passing functions around.
Practical Example 1 Link to heading
cmd/cli/main.go
package main
import "fmt"
func main() {
// Commands are just functions
var history []func()
execute := func(cmd func()) {
cmd()
history = append(history, cmd)
}
// Execute commands
execute(func() { fmt.Println("creating user") })
execute(func() { fmt.Println("sending email") })
execute(func() { fmt.Println("updating inventory") })
// Replay history
fmt.Println("\n--- replaying ---")
for _, cmd := range history {
cmd()
}
}
Practical Example 2 Link to heading
internal/editor/editor.go
package editor
type Command interface {
Execute()
Undo()
}
type Editor struct {
content string
history []Command
}
func NewEditor() *Editor {
return &Editor{}
}
func (e *Editor) Content() string {
return e.content
}
func (e *Editor) Execute(cmd Command) {
cmd.Execute()
e.history = append(e.history, cmd)
}
func (e *Editor) Undo() {
if len(e.history) == 0 {
return
}
last := e.history[len(e.history)-1]
last.Undo()
e.history = e.history[:len(e.history)-1]
}
// Insert command
type InsertCommand struct {
editor *Editor
position int
text string
}
func NewInsertCommand(e *Editor, pos int, text string) *InsertCommand {
return &InsertCommand{editor: e, position: pos, text: text}
}
func (c *InsertCommand) Execute() {
if c.position >= len(c.editor.content) {
c.editor.content += c.text
} else {
c.editor.content = c.editor.content[:c.position] + c.text + c.editor.content[c.position:]
}
}
func (c *InsertCommand) Undo() {
end := c.position + len(c.text)
c.editor.content = c.editor.content[:c.position] + c.editor.content[end:]
}
// Delete command
type DeleteCommand struct {
editor *Editor
position int
length int
deleted string
}
func NewDeleteCommand(e *Editor, pos, length int) *DeleteCommand {
return &DeleteCommand{editor: e, position: pos, length: length}
}
func (c *DeleteCommand) Execute() {
end := c.position + c.length
if end > len(c.editor.content) {
end = len(c.editor.content)
}
c.deleted = c.editor.content[c.position:end]
c.editor.content = c.editor.content[:c.position] + c.editor.content[end:]
}
func (c *DeleteCommand) Undo() {
c.editor.content = c.editor.content[:c.position] + c.deleted + c.editor.content[c.position:]
}
cmd/editor/main.go
package main
import (
"fmt"
"myapp/internal/editor"
)
func main() {
e := editor.NewEditor()
// Execute commands
e.Execute(editor.NewInsertCommand(e, 0, "Hello"))
fmt.Println(e.Content()) // Hello
e.Execute(editor.NewInsertCommand(e, 5, " World"))
fmt.Println(e.Content()) // Hello World
e.Execute(editor.NewDeleteCommand(e, 5, 6))
fmt.Println(e.Content()) // Hello
// Undo
e.Undo()
fmt.Println(e.Content()) // Hello World
e.Undo()
fmt.Println(e.Content()) // Hello
}
Considerations Link to heading
Example 1: In Go, functions are first-class values. When you just need to queue or log operations, a slice of functions is often enough. No interface ceremony required.
Example 2: The full Command pattern with an interface is useful when commands need to carry state for undo/redo, or when you need to serialize commands for logging or replay. The extra structure pays off when commands are more than simple actions.
If you don’t need undo or command state, prefer Example 1. If you need to reverse operations or inspect command history, Example 2 is worth the added complexity.
Verdict Link to heading
- Example 1: Idiomatic
- Example 2: Idiomatic (when undo/redo is required)
Interpreter Pattern Link to heading
Problem Space Link to heading
The Interpreter pattern defines a grammar for a language and provides an interpreter to evaluate sentences in that language. It’s useful for parsing and evaluating expressions, query languages, or domain-specific languages. In Go, this is often seen in template engines, expression evaluators, and query builders.
Practical Example Link to heading
cmd/template/main.go
package main
import (
"os"
"text/template"
)
func main() {
// text/template is an interpreter for a template language
tmpl, err := template.New("greeting").Parse("Hello, {{.Name}}! You have {{.Count}} messages.\n")
if err != nil {
panic(err)
}
data := struct {
Name string
Count int
}{
Name: "Alice",
Count: 5,
}
tmpl.Execute(os.Stdout, data)
}
Practical Example 2 Link to heading
internal/expr/expr.go
package expr
import "fmt"
type Expression interface {
Interpret(vars map[string]int) int
}
// Terminal: number literal
type Number struct {
value int
}
func NewNumber(v int) *Number {
return &Number{value: v}
}
func (n *Number) Interpret(vars map[string]int) int {
return n.value
}
// Terminal: variable
type Variable struct {
name string
}
func NewVariable(name string) *Variable {
return &Variable{name: name}
}
func (v *Variable) Interpret(vars map[string]int) int {
return vars[v.name]
}
// Non-terminal: addition
type Add struct {
left, right Expression
}
func NewAdd(left, right Expression) *Add {
return &Add{left: left, right: right}
}
func (a *Add) Interpret(vars map[string]int) int {
return a.left.Interpret(vars) + a.right.Interpret(vars)
}
// Non-terminal: multiplication
type Multiply struct {
left, right Expression
}
func NewMultiply(left, right Expression) *Multiply {
return &Multiply{left: left, right: right}
}
func (m *Multiply) Interpret(vars map[string]int) int {
return m.left.Interpret(vars) * m.right.Interpret(vars)
}
cmd/calc/main.go
package main
import (
"fmt"
"myapp/internal/expr"
)
func main() {
// Build expression: (x + 5) * y
expression := expr.NewMultiply(
expr.NewAdd(
expr.NewVariable("x"),
expr.NewNumber(5),
),
expr.NewVariable("y"),
)
vars := map[string]int{
"x": 10,
"y": 3,
}
result := expression.Interpret(vars)
fmt.Printf("(x + 5) * y = %d\n", result) // (10 + 5) * 3 = 45
// Reuse with different values
vars["x"] = 2
vars["y"] = 4
result = expression.Interpret(vars)
fmt.Printf("(x + 5) * y = %d\n", result) // (2 + 5) * 4 = 28
}
Considerations Link to heading
Example 1: The standard library’s text/template and html/template packages are full implementations of the Interpreter pattern. They parse a template language and evaluate it against data. Most of the time, using these is enough.
Example 2: Building your own interpreter makes sense for domain-specific languages like expression evaluators, rule engines, or query builders. The tree structure of expressions maps naturally to Go interfaces.
The Interpreter pattern is rarely needed directly. Before building one, check if text/template, regexp, or an existing parser library solves your problem. Custom interpreters add complexity and maintenance burden.
Verdict Link to heading
- Example 1: Idiomatic
- Example 2: Code Smell (build only when existing tools don’t fit)
Iterator Pattern Link to heading
Problem Space Link to heading
The Iterator pattern provides a way to access elements of a collection sequentially without exposing the underlying representation. In Go, the language itself provides iteration through range, and channels can serve as iterators. Go 1.23 introduced the iter package for more flexible iteration.
Practical Example 1 Link to heading
cmd/iter/main.go
package main
import "fmt"
func main() {
// range is Go's built-in iterator
nums := []int{1, 2, 3, 4, 5}
for i, v := range nums {
fmt.Printf("index: %d, value: %d\n", i, v)
}
// maps too
users := map[string]int{"alice": 30, "bob": 25}
for name, age := range users {
fmt.Printf("%s is %d\n", name, age)
}
}
Practical Example 2 Link to heading
internal/pipeline/pipeline.go
package pipeline
func Generate(nums ...int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for _, n := range nums {
out <- n
}
}()
return out
}
func Filter(in <-chan int, predicate func(int) bool) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
if predicate(n) {
out <- n
}
}
}()
return out
}
func Map(in <-chan int, transform func(int) int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
out <- transform(n)
}
}()
return out
}
cmd/pipeline/main.go
package main
import (
"fmt"
"myapp/internal/pipeline"
)
func main() {
// Channel-based iterator pipeline
nums := pipeline.Generate(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
evens := pipeline.Filter(nums, func(n int) bool {
return n%2 == 0
})
doubled := pipeline.Map(evens, func(n int) int {
return n * 2
})
// Consume the iterator
for n := range doubled {
fmt.Println(n) // 4, 8, 12, 16, 20
}
}
Practical Example 3 Link to heading
internal/collection/collection.go
package collection
import "iter"
type Stack[T any] struct {
items []T
}
func NewStack[T any]() *Stack[T] {
return &Stack[T]{}
}
func (s *Stack[T]) Push(item T) {
s.items = append(s.items, item)
}
func (s *Stack[T]) Pop() (T, bool) {
if len(s.items) == 0 {
var zero T
return zero, false
}
item := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return item, true
}
// Go 1.23+ iterator
func (s *Stack[T]) All() iter.Seq[T] {
return func(yield func(T) bool) {
for i := len(s.items) - 1; i >= 0; i-- {
if !yield(s.items[i]) {
return
}
}
}
}
cmd/stack/main.go
package main
import (
"fmt"
"myapp/internal/collection"
)
func main() {
stack := collection.NewStack[string]()
stack.Push("first")
stack.Push("second")
stack.Push("third")
// Iterate without popping
for item := range stack.All() {
fmt.Println(item) // third, second, first
}
}
Considerations Link to heading
Example 1: The range keyword is Go’s built-in iterator. It works with slices, arrays, maps, strings, and channels. For most cases, this is all you need.
Example 2: Channels as iterators enable concurrent pipelines. Each stage runs in its own goroutine, processing elements as they flow through. Useful for stream processing but adds goroutine overhead.
Example 3: Go 1.23 introduced iter.Seq for custom iterators that work with range. This is the idiomatic way to make custom collections iterable without exposing internals.
Go’s philosophy is that iteration should be simple. Reach for range first, channels for concurrent pipelines, and iter.Seq for custom collections.
Verdict Link to heading
- Example 1: Idiomatic
- Example 2: Idiomatic (for concurrent pipelines)
- Example 3: Idiomatic (Go 1.23+)
Mediator Pattern Link to heading
Problem Space Link to heading
The Mediator pattern defines an object that encapsulates how a set of objects interact. Instead of components communicating directly with each other, they communicate through a central mediator. This reduces coupling between components and makes their interactions easier to manage.
Practical Example 1 Link to heading
internal/chat/chat.go
package chat
import (
"fmt"
"sync"
)
type Room struct {
mu sync.RWMutex
users map[string]chan string
}
func NewRoom() *Room {
return &Room{
users: make(map[string]chan string),
}
}
func (r *Room) Join(name string) <-chan string {
ch := make(chan string, 10)
r.mu.Lock()
r.users[name] = ch
r.mu.Unlock()
r.broadcast(name, name+" has joined")
return ch
}
func (r *Room) Leave(name string) {
r.mu.Lock()
ch, ok := r.users[name]
if ok {
close(ch)
delete(r.users, name)
}
r.mu.Unlock()
if ok {
r.broadcast(name, name+" has left")
}
}
func (r *Room) Send(from, message string) {
r.broadcast(from, fmt.Sprintf("%s: %s", from, message))
}
func (r *Room) broadcast(from, message string) {
r.mu.RLock()
defer r.mu.RUnlock()
for name, ch := range r.users {
if name != from {
select {
case ch <- message:
default:
// channel full, skip
}
}
}
}
cmd/chat/main.go
package main
import (
"fmt"
"myapp/internal/chat"
"time"
)
func main() {
room := chat.NewRoom()
// Users join through the mediator, not directly to each other
aliceCh := room.Join("alice")
bobCh := room.Join("bob")
// Listen for messages
go func() {
for msg := range aliceCh {
fmt.Println("alice received:", msg)
}
}()
go func() {
for msg := range bobCh {
fmt.Println("bob received:", msg)
}
}()
time.Sleep(10 * time.Millisecond)
// Send through mediator
room.Send("alice", "hello everyone")
room.Send("bob", "hey alice")
time.Sleep(10 * time.Millisecond)
room.Leave("alice")
time.Sleep(10 * time.Millisecond)
}
Practical Example 2 Link to heading
internal/events/events.go
package events
import "sync"
type EventBus struct {
mu sync.RWMutex
handlers map[string][]func(any)
}
func NewEventBus() *EventBus {
return &EventBus{
handlers: make(map[string][]func(any)),
}
}
func (b *EventBus) Subscribe(event string, handler func(any)) {
b.mu.Lock()
defer b.mu.Unlock()
b.handlers[event] = append(b.handlers[event], handler)
}
func (b *EventBus) Publish(event string, data any) {
b.mu.RLock()
defer b.mu.RUnlock()
for _, handler := range b.handlers[event] {
handler(data)
}
}
cmd/events/main.go
package main
import (
"fmt"
"myapp/internal/events"
)
type OrderService struct {
bus *events.EventBus
}
func (s *OrderService) CreateOrder(id string) {
fmt.Println("order created:", id)
s.bus.Publish("order.created", id)
}
type InventoryService struct{}
func (s *InventoryService) OnOrderCreated(data any) {
fmt.Println("inventory: reserving stock for order", data)
}
type EmailService struct{}
func (s *EmailService) OnOrderCreated(data any) {
fmt.Println("email: sending confirmation for order", data)
}
func main() {
// Event bus is the mediator
bus := events.NewEventBus()
orderSvc := &OrderService{bus: bus}
inventorySvc := &InventoryService{}
emailSvc := &EmailService{}
// Services subscribe through mediator
bus.Subscribe("order.created", inventorySvc.OnOrderCreated)
bus.Subscribe("order.created", emailSvc.OnOrderCreated)
// Order service doesn't know about inventory or email
orderSvc.CreateOrder("ORD-123")
}
Considerations Link to heading
Example 1: A chat room is a classic mediator. Users don’t send messages directly to each other. They send to the room, and the room broadcasts to others. Adding or removing users doesn’t affect other users.
Example 2: An event bus decouples services completely. The order service publishes events without knowing who consumes them. New subscribers can be added without modifying existing code.
The Mediator pattern trades direct coupling for indirect coupling through the mediator. This simplifies component interactions but the mediator itself can become complex. Keep mediators focused on coordination, not business logic.
Verdict Link to heading
- Example 1: Idiomatic
- Example 2: Idiomatic
Memento Pattern Link to heading
Problem Space Link to heading
The Memento pattern captures an object’s internal state so it can be restored later, without exposing the details of its implementation. This is useful for undo/redo functionality, checkpoints, save states, and transaction rollback.
Practical Example 1 Link to heading
internal/editor/editor.go
package editor
type Editor struct {
content string
history []string
index int
}
func NewEditor() *Editor {
return &Editor{
history: []string{""},
index: 0,
}
}
func (e *Editor) Type(text string) {
e.content += text
e.save()
}
func (e *Editor) Delete(n int) {
if n > len(e.content) {
n = len(e.content)
}
e.content = e.content[:len(e.content)-n]
e.save()
}
func (e *Editor) Content() string {
return e.content
}
func (e *Editor) save() {
// Truncate any redo history
e.history = e.history[:e.index+1]
e.history = append(e.history, e.content)
e.index = len(e.history) - 1
}
func (e *Editor) Undo() {
if e.index > 0 {
e.index--
e.content = e.history[e.index]
}
}
func (e *Editor) Redo() {
if e.index < len(e.history)-1 {
e.index++
e.content = e.history[e.index]
}
}
cmd/editor/main.go
package main
import (
"fmt"
"myapp/internal/editor"
)
func main() {
e := editor.NewEditor()
e.Type("Hello")
fmt.Println(e.Content()) // Hello
e.Type(" World")
fmt.Println(e.Content()) // Hello World
e.Delete(6)
fmt.Println(e.Content()) // Hello
e.Undo()
fmt.Println(e.Content()) // Hello World
e.Undo()
fmt.Println(e.Content()) // Hello
e.Redo()
fmt.Println(e.Content()) // Hello World
}
Practical Example 2 Link to heading
internal/game/game.go
package game
import "encoding/json"
type GameState struct {
Level int `json:"level"`
Score int `json:"score"`
Health int `json:"health"`
Inventory []string `json:"inventory"`
}
type Game struct {
state GameState
}
func NewGame() *Game {
return &Game{
state: GameState{
Level: 1,
Score: 0,
Health: 100,
Inventory: []string{},
},
}
}
func (g *Game) Play() {
g.state.Score += 100
g.state.Health -= 10
}
func (g *Game) LevelUp() {
g.state.Level++
g.state.Health = 100
}
func (g *Game) AddItem(item string) {
g.state.Inventory = append(g.state.Inventory, item)
}
func (g *Game) State() GameState {
return g.state
}
// Save creates a memento
func (g *Game) Save() ([]byte, error) {
return json.Marshal(g.state)
}
// Restore applies a memento
func (g *Game) Restore(memento []byte) error {
return json.Unmarshal(memento, &g.state)
}
cmd/game/main.go
package main
import (
"fmt"
"myapp/internal/game"
)
func main() {
g := game.NewGame()
g.Play()
g.AddItem("sword")
fmt.Printf("before save: %+v\n", g.State())
// Save checkpoint
checkpoint, err := g.Save()
if err != nil {
panic(err)
}
// Keep playing
g.Play()
g.Play()
g.LevelUp()
g.AddItem("shield")
fmt.Printf("after more play: %+v\n", g.State())
// Restore to checkpoint
if err := g.Restore(checkpoint); err != nil {
panic(err)
}
fmt.Printf("after restore: %+v\n", g.State())
}
Considerations Link to heading
Example 1: History stored as simple string snapshots. The editor manages its own history internally. This is straightforward for types that are cheap to copy.
Example 2: Serialization as memento. Using JSON (or gob, protobuf, etc.) to capture state allows saving to disk, sending over network, or storing in a database. The memento is opaque bytes that only the originator knows how to interpret.
For simple undo/redo with value types, store copies directly. For complex state or persistence needs, serialize to bytes. Both approaches keep the state encapsulated within the object.
Verdict Link to heading
- Example 1: Idiomatic
- Example 2: Idiomatic
Observer Pattern Link to heading
Problem Space Link to heading
The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. In Go, this is commonly implemented with channels or callback functions.
Practical Example 1 Link to heading
internal/stock/stock.go
package stock
import "sync"
type PriceUpdate struct {
Symbol string
Price float64
}
type Ticker struct {
mu sync.RWMutex
symbol string
price float64
observers []chan PriceUpdate
}
func NewTicker(symbol string, price float64) *Ticker {
return &Ticker{
symbol: symbol,
price: price,
}
}
func (t *Ticker) Subscribe() <-chan PriceUpdate {
ch := make(chan PriceUpdate, 1)
t.mu.Lock()
t.observers = append(t.observers, ch)
t.mu.Unlock()
return ch
}
func (t *Ticker) Unsubscribe(ch <-chan PriceUpdate) {
t.mu.Lock()
defer t.mu.Unlock()
for i, obs := range t.observers {
if obs == ch {
close(obs)
t.observers = append(t.observers[:i], t.observers[i+1:]...)
return
}
}
}
func (t *Ticker) SetPrice(price float64) {
t.mu.Lock()
t.price = price
update := PriceUpdate{Symbol: t.symbol, Price: price}
observers := make([]chan PriceUpdate, len(t.observers))
copy(observers, t.observers)
t.mu.Unlock()
for _, ch := range observers {
select {
case ch <- update:
default:
// observer not ready, skip
}
}
}
cmd/stock/main.go
package main
import (
"fmt"
"myapp/internal/stock"
"time"
)
func main() {
ticker := stock.NewTicker("GOOG", 150.00)
// Observer 1
ch1 := ticker.Subscribe()
go func() {
for update := range ch1 {
fmt.Printf("observer 1: %s = $%.2f\n", update.Symbol, update.Price)
}
}()
// Observer 2
ch2 := ticker.Subscribe()
go func() {
for update := range ch2 {
fmt.Printf("observer 2: %s = $%.2f\n", update.Symbol, update.Price)
}
}()
time.Sleep(10 * time.Millisecond)
ticker.SetPrice(151.50)
ticker.SetPrice(152.25)
time.Sleep(10 * time.Millisecond)
ticker.Unsubscribe(ch1)
ticker.SetPrice(153.00) // only observer 2 receives this
time.Sleep(10 * time.Millisecond)
}
Practical Example 2 Link to heading
internal/config/config.go
package config
import "sync"
type Config struct {
mu sync.RWMutex
values map[string]string
listeners []func(key, value string)
}
func New() *Config {
return &Config{
values: make(map[string]string),
}
}
func (c *Config) OnChange(fn func(key, value string)) {
c.mu.Lock()
c.listeners = append(c.listeners, fn)
c.mu.Unlock()
}
func (c *Config) Set(key, value string) {
c.mu.Lock()
c.values[key] = value
listeners := make([]func(key, value string), len(c.listeners))
copy(listeners, c.listeners)
c.mu.Unlock()
for _, fn := range listeners {
fn(key, value)
}
}
func (c *Config) Get(key string) string {
c.mu.RLock()
defer c.mu.RUnlock()
return c.values[key]
}
cmd/config/main.go
package main
import (
"fmt"
"myapp/internal/config"
)
func main() {
cfg := config.New()
// Register observers
cfg.OnChange(func(key, value string) {
fmt.Printf("logger: config changed %s=%s\n", key, value)
})
cfg.OnChange(func(key, value string) {
if key == "debug" && value == "true" {
fmt.Println("debug mode enabled")
}
})
// Changes notify all observers
cfg.Set("database_url", "postgres://localhost/app")
cfg.Set("debug", "true")
cfg.Set("port", "8080")
}
Considerations Link to heading
Example 1: Channel-based observers are natural in Go. Each observer gets its own channel and can process updates concurrently. Unsubscribing closes the channel, signaling the observer to stop. Good for concurrent or long-running observers.
Example 2: Callback-based observers are simpler when notifications are synchronous and short-lived. The subject calls each function directly. Good for configuration changes, validation hooks, or event logging.
Channels are better when observers need to process updates independently or at their own pace. Callbacks are better when you need synchronous notification and simpler code.
Verdict Link to heading
- Example 1: Idiomatic
- Example 2: Idiomatic
State Pattern Link to heading
Problem Space Link to heading
The State pattern allows an object to alter its behavior when its internal state changes. Instead of large conditional blocks checking state, you encapsulate state-specific behavior in separate types. The object delegates behavior to its current state.
Practical Example 1 Link to heading
internal/order/order.go
package order
import "fmt"
type State interface {
Process(o *Order) error
Cancel(o *Order) error
Ship(o *Order) error
String() string
}
type Order struct {
ID string
state State
}
func NewOrder(id string) *Order {
return &Order{
ID: id,
state: &PendingState{},
}
}
func (o *Order) SetState(s State) {
o.state = s
}
func (o *Order) Process() error {
return o.state.Process(o)
}
func (o *Order) Cancel() error {
return o.state.Cancel(o)
}
func (o *Order) Ship() error {
return o.state.Ship(o)
}
func (o *Order) Status() string {
return o.state.String()
}
// Pending state
type PendingState struct{}
func (s *PendingState) Process(o *Order) error {
fmt.Println("processing payment...")
o.SetState(&PaidState{})
return nil
}
func (s *PendingState) Cancel(o *Order) error {
fmt.Println("order cancelled")
o.SetState(&CancelledState{})
return nil
}
func (s *PendingState) Ship(o *Order) error {
return fmt.Errorf("cannot ship: payment not processed")
}
func (s *PendingState) String() string {
return "pending"
}
// Paid state
type PaidState struct{}
func (s *PaidState) Process(o *Order) error {
return fmt.Errorf("already processed")
}
func (s *PaidState) Cancel(o *Order) error {
fmt.Println("refunding payment...")
o.SetState(&CancelledState{})
return nil
}
func (s *PaidState) Ship(o *Order) error {
fmt.Println("shipping order...")
o.SetState(&ShippedState{})
return nil
}
func (s *PaidState) String() string {
return "paid"
}
// Shipped state
type ShippedState struct{}
func (s *ShippedState) Process(o *Order) error {
return fmt.Errorf("already processed")
}
func (s *ShippedState) Cancel(o *Order) error {
return fmt.Errorf("cannot cancel: already shipped")
}
func (s *ShippedState) Ship(o *Order) error {
return fmt.Errorf("already shipped")
}
func (s *ShippedState) String() string {
return "shipped"
}
// Cancelled state
type CancelledState struct{}
func (s *CancelledState) Process(o *Order) error {
return fmt.Errorf("order cancelled")
}
func (s *CancelledState) Cancel(o *Order) error {
return fmt.Errorf("already cancelled")
}
func (s *CancelledState) Ship(o *Order) error {
return fmt.Errorf("order cancelled")
}
func (s *CancelledState) String() string {
return "cancelled"
}
cmd/order/main.go
package main
import (
"fmt"
"myapp/internal/order"
)
func main() {
o := order.NewOrder("ORD-123")
fmt.Println("status:", o.Status()) // pending
// Try to ship before payment
if err := o.Ship(); err != nil {
fmt.Println("error:", err)
}
// Process payment
o.Process()
fmt.Println("status:", o.Status()) // paid
// Ship order
o.Ship()
fmt.Println("status:", o.Status()) // shipped
// Try to cancel shipped order
if err := o.Cancel(); err != nil {
fmt.Println("error:", err)
}
}
Practical Example 2 Link to heading
internal/connection/connection.go
package connection
import "fmt"
type State int
const (
Disconnected State = iota
Connecting
Connected
)
type Connection struct {
state State
address string
}
func New(address string) *Connection {
return &Connection{
state: Disconnected,
address: address,
}
}
func (c *Connection) Connect() error {
switch c.state {
case Disconnected:
fmt.Println("connecting to", c.address)
c.state = Connecting
// simulate connection
c.state = Connected
fmt.Println("connected")
return nil
case Connecting:
return fmt.Errorf("already connecting")
case Connected:
return fmt.Errorf("already connected")
default:
return fmt.Errorf("unknown state")
}
}
func (c *Connection) Send(data string) error {
switch c.state {
case Connected:
fmt.Println("sending:", data)
return nil
case Connecting:
return fmt.Errorf("not ready: still connecting")
case Disconnected:
return fmt.Errorf("not connected")
default:
return fmt.Errorf("unknown state")
}
}
func (c *Connection) Disconnect() error {
switch c.state {
case Connected, Connecting:
fmt.Println("disconnecting")
c.state = Disconnected
return nil
case Disconnected:
return fmt.Errorf("already disconnected")
default:
return fmt.Errorf("unknown state")
}
}
func (c *Connection) Status() string {
switch c.state {
case Disconnected:
return "disconnected"
case Connecting:
return "connecting"
case Connected:
return "connected"
default:
return "unknown"
}
}
cmd/connection/main.go
package main
import (
"fmt"
"myapp/internal/connection"
)
func main() {
conn := connection.New("localhost:8080")
fmt.Println("status:", conn.Status()) // disconnected
// Try to send before connecting
if err := conn.Send("hello"); err != nil {
fmt.Println("error:", err)
}
conn.Connect()
fmt.Println("status:", conn.Status()) // connected
conn.Send("hello")
conn.Send("world")
conn.Disconnect()
fmt.Println("status:", conn.Status()) // disconnected
}
Considerations Link to heading
Example 1: Full State pattern with separate types for each state. Each state encapsulates its own transition logic. Adding new states doesn’t require modifying existing ones. Good for complex state machines with many states and transitions.
Example 2: Simple state with switch statements. When you have few states and transitions, an enum with switches is clearer and more direct. Adding a new state requires updating each switch, but for small state machines this is manageable.
Start with Example 2 for simple cases. Move to Example 1 when switch statements become unwieldy or when states need their own data.
Verdict Link to heading
- Example 1: Idiomatic (for complex state machines)
- Example 2: Idiomatic (for simple state machines)
Strategy Pattern Link to heading
Problem Space Link to heading
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. It lets you select an algorithm at runtime without changing the code that uses it. In Go, this is naturally expressed through interfaces or function types.
Practical Example 1 Link to heading
internal/compression/compression.go
package compression
import (
"bytes"
"compress/gzip"
"compress/zlib"
"io"
)
type Compressor interface {
Compress(data []byte) ([]byte, error)
}
type GzipCompressor struct{}
func (c *GzipCompressor) Compress(data []byte) ([]byte, error) {
var buf bytes.Buffer
w := gzip.NewWriter(&buf)
if _, err := w.Write(data); err != nil {
return nil, err
}
if err := w.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
type ZlibCompressor struct{}
func (c *ZlibCompressor) Compress(data []byte) ([]byte, error) {
var buf bytes.Buffer
w := zlib.NewWriter(&buf)
if _, err := w.Write(data); err != nil {
return nil, err
}
if err := w.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
type NoCompressor struct{}
func (c *NoCompressor) Compress(data []byte) ([]byte, error) {
return data, nil
}
cmd/compress/main.go
package main
import (
"fmt"
"myapp/internal/compression"
)
type FileWriter struct {
compressor compression.Compressor
}
func NewFileWriter(c compression.Compressor) *FileWriter {
return &FileWriter{compressor: c}
}
func (w *FileWriter) Write(data []byte) error {
compressed, err := w.compressor.Compress(data)
if err != nil {
return err
}
fmt.Printf("writing %d bytes (original: %d)\n", len(compressed), len(data))
return nil
}
func main() {
data := []byte("hello world, this is some test data for compression")
// Use gzip strategy
gzipWriter := NewFileWriter(&compression.GzipCompressor{})
gzipWriter.Write(data)
// Use zlib strategy
zlibWriter := NewFileWriter(&compression.ZlibCompressor{})
zlibWriter.Write(data)
// Use no compression
plainWriter := NewFileWriter(&compression.NoCompressor{})
plainWriter.Write(data)
}
Practical Example 2 Link to heading
internal/pricing/pricing.go
package pricing
type PriceStrategy func(basePrice float64, quantity int) float64
func RegularPrice(basePrice float64, quantity int) float64 {
return basePrice * float64(quantity)
}
func BulkDiscount(basePrice float64, quantity int) float64 {
total := basePrice * float64(quantity)
if quantity >= 10 {
return total * 0.9 // 10% off
}
return total
}
func PremiumMember(basePrice float64, quantity int) float64 {
return basePrice * float64(quantity) * 0.85 // 15% off always
}
type Cart struct {
strategy PriceStrategy
items []item
}
type item struct {
price float64
quantity int
}
func NewCart(strategy PriceStrategy) *Cart {
return &Cart{strategy: strategy}
}
func (c *Cart) SetStrategy(strategy PriceStrategy) {
c.strategy = strategy
}
func (c *Cart) Add(price float64, quantity int) {
c.items = append(c.items, item{price, quantity})
}
func (c *Cart) Total() float64 {
var total float64
for _, item := range c.items {
total += c.strategy(item.price, item.quantity)
}
return total
}
cmd/pricing/main.go
package main
import (
"fmt"
"myapp/internal/pricing"
)
func main() {
cart := pricing.NewCart(pricing.RegularPrice)
cart.Add(10.00, 5)
cart.Add(25.00, 3)
fmt.Printf("regular: $%.2f\n", cart.Total()) // 125.00
// Switch strategy
cart.SetStrategy(pricing.BulkDiscount)
cart.Add(10.00, 10)
fmt.Printf("bulk: $%.2f\n", cart.Total()) // 215.00
// Premium member
premiumCart := pricing.NewCart(pricing.PremiumMember)
premiumCart.Add(10.00, 5)
premiumCart.Add(25.00, 3)
fmt.Printf("premium: $%.2f\n", premiumCart.Total()) // 106.25
}
Considerations Link to heading
Example 1: Interface-based strategy. Each algorithm is a type implementing an interface. Good when strategies need their own configuration or state, or when you want to mock them in tests.
Example 2: Function-based strategy. When the algorithm is a pure function with no state, a function type is simpler and more direct. No need to create types just to hold a method.
In Go, prefer function types for stateless strategies. Use interfaces when strategies need configuration, dependencies, or multiple methods.
Verdict Link to heading
- Example 1: Idiomatic
- Example 2: Idiomatic
Template Method Pattern Link to heading
Problem Space Link to heading
The Template Method pattern defines the skeleton of an algorithm, deferring some steps to be implemented by the caller. The overall structure stays fixed while specific steps vary. In Go, without inheritance, this is achieved through interfaces or function parameters.
Practical Example 1 Link to heading
internal/report/report.go
package report
import (
"fmt"
"io"
"time"
)
type DataFetcher interface {
Fetch() ([]byte, error)
}
type Generator struct {
title string
fetcher DataFetcher
}
func NewGenerator(title string, fetcher DataFetcher) *Generator {
return &Generator{title: title, fetcher: fetcher}
}
// Generate is the template method
func (g *Generator) Generate(w io.Writer) error {
// Step 1: Header (fixed)
fmt.Fprintf(w, "Report: %s\n", g.title)
fmt.Fprintf(w, "Generated: %s\n", time.Now().Format(time.RFC3339))
fmt.Fprintln(w, "---")
// Step 2: Fetch data (variable)
data, err := g.fetcher.Fetch()
if err != nil {
return err
}
// Step 3: Write body (fixed)
fmt.Fprintln(w, string(data))
// Step 4: Footer (fixed)
fmt.Fprintln(w, "---")
fmt.Fprintln(w, "End of Report")
return nil
}
// Implementations
type DBFetcher struct {
query string
}
func NewDBFetcher(query string) *DBFetcher {
return &DBFetcher{query: query}
}
func (f *DBFetcher) Fetch() ([]byte, error) {
// simulate database query
return []byte(fmt.Sprintf("Results for: %s\nRow 1\nRow 2\nRow 3", f.query)), nil
}
type APIFetcher struct {
endpoint string
}
func NewAPIFetcher(endpoint string) *APIFetcher {
return &APIFetcher{endpoint: endpoint}
}
func (f *APIFetcher) Fetch() ([]byte, error) {
// simulate API call
return []byte(fmt.Sprintf("Response from: %s\n{\"status\": \"ok\"}", f.endpoint)), nil
}
cmd/report/main.go
package main
import (
"os"
"fmt"
"myapp/internal/report"
)
func main() {
// Same template, different data sources
dbReport := report.NewGenerator(
"Database Summary",
report.NewDBFetcher("SELECT * FROM users"),
)
dbReport.Generate(os.Stdout)
fmt.Println()
apiReport := report.NewGenerator(
"API Status",
report.NewAPIFetcher("https://api.example.com/health"),
)
apiReport.Generate(os.Stdout)
}
Practical Example 2 Link to heading
internal/pipeline/pipeline.go
package pipeline
import "fmt"
type ProcessFunc func([]byte) ([]byte, error)
type Pipeline struct {
validate ProcessFunc
transform ProcessFunc
store ProcessFunc
}
func New(validate, transform, store ProcessFunc) *Pipeline {
return &Pipeline{
validate: validate,
transform: transform,
store: store,
}
}
// Run is the template method
func (p *Pipeline) Run(data []byte) error {
fmt.Println("starting pipeline...")
// Step 1: Validate (variable)
validated, err := p.validate(data)
if err != nil {
return fmt.Errorf("validation failed: %w", err)
}
fmt.Println("validation passed")
// Step 2: Transform (variable)
transformed, err := p.transform(validated)
if err != nil {
return fmt.Errorf("transform failed: %w", err)
}
fmt.Printf("transformed %d bytes\n", len(transformed))
// Step 3: Store (variable)
_, err = p.store(transformed)
if err != nil {
return fmt.Errorf("store failed: %w", err)
}
fmt.Println("stored successfully")
fmt.Println("pipeline complete")
return nil
}
cmd/pipeline/main.go
package main
import (
"bytes"
"fmt"
"myapp/internal/pipeline"
"strings"
)
func main() {
// Define steps as functions
validate := func(data []byte) ([]byte, error) {
if len(data) == 0 {
return nil, fmt.Errorf("empty data")
}
return data, nil
}
uppercase := func(data []byte) ([]byte, error) {
return []byte(strings.ToUpper(string(data))), nil
}
store := func(data []byte) ([]byte, error) {
fmt.Printf("storing: %s\n", data)
return data, nil
}
p := pipeline.New(validate, uppercase, store)
p.Run([]byte("hello world"))
fmt.Println()
// Different pipeline with different steps
compress := func(data []byte) ([]byte, error) {
return bytes.ReplaceAll(data, []byte(" "), []byte("")), nil
}
p2 := pipeline.New(validate, compress, store)
p2.Run([]byte("hello world"))
}
Considerations Link to heading
Example 1: Interface-based template method. The fixed algorithm calls interface methods for variable steps. Good when the variable part has multiple implementations that need their own state or configuration.
Example 2: Function-based template method. Pass functions directly for each variable step. More flexible and concise when steps are simple transformations without state.
The standard library’s sort.Interface is a template method. You provide Len, Less, and Swap, and sort.Sort handles the algorithm. This pattern is common in Go whenever you need the caller to customize parts of a fixed process.
Verdict Link to heading
- Example 1: Idiomatic
- Example 2: Idiomatic
Visitor Pattern Link to heading
Problem Space Link to heading
The Visitor pattern lets you add new operations to existing types without modifying them. You define a visitor interface that has a method for each type, and each type accepts a visitor and calls the appropriate method. This separates algorithms from the objects they operate on.
Practical Example 1 Link to heading
internal/ast/ast.go
package ast
import "fmt"
type Visitor interface {
VisitNumber(n *Number)
VisitBinary(b *Binary)
}
type Node interface {
Accept(v Visitor)
}
type Number struct {
Value int
}
func (n *Number) Accept(v Visitor) {
v.VisitNumber(n)
}
type Binary struct {
Left Node
Operator string
Right Node
}
func (b *Binary) Accept(v Visitor) {
v.VisitBinary(b)
}
// Printer visitor
type Printer struct{}
func (p *Printer) VisitNumber(n *Number) {
fmt.Print(n.Value)
}
func (p *Printer) VisitBinary(b *Binary) {
fmt.Print("(")
b.Left.Accept(p)
fmt.Printf(" %s ", b.Operator)
b.Right.Accept(p)
fmt.Print(")")
}
// Evaluator visitor
type Evaluator struct {
Result int
}
func (e *Evaluator) VisitNumber(n *Number) {
e.Result = n.Value
}
func (e *Evaluator) VisitBinary(b *Binary) {
leftEval := &Evaluator{}
b.Left.Accept(leftEval)
rightEval := &Evaluator{}
b.Right.Accept(rightEval)
switch b.Operator {
case "+":
e.Result = leftEval.Result + rightEval.Result
case "-":
e.Result = leftEval.Result - rightEval.Result
case "*":
e.Result = leftEval.Result * rightEval.Result
case "/":
e.Result = leftEval.Result / rightEval.Result
}
}
cmd/ast/main.go
package main
import (
"fmt"
"myapp/internal/ast"
)
func main() {
// Build expression: (1 + 2) * 3
expr := &ast.Binary{
Left: &ast.Binary{
Left: &ast.Number{Value: 1},
Operator: "+",
Right: &ast.Number{Value: 2},
},
Operator: "*",
Right: &ast.Number{Value: 3},
}
// Print visitor
printer := &ast.Printer{}
expr.Accept(printer)
fmt.Println()
// Evaluate visitor
eval := &ast.Evaluator{}
expr.Accept(eval)
fmt.Println("=", eval.Result)
}
Practical Example 2 Link to heading
internal/filesystem/filesystem.go
package filesystem
type Visitor func(path string, isDir bool, size int64)
type Entry interface {
Accept(v Visitor, path string)
}
type File struct {
Name string
Size int64
}
func (f *File) Accept(v Visitor, path string) {
v(path+"/"+f.Name, false, f.Size)
}
type Directory struct {
Name string
Entries []Entry
}
func (d *Directory) Accept(v Visitor, path string) {
currentPath := path + "/" + d.Name
v(currentPath, true, 0)
for _, entry := range d.Entries {
entry.Accept(v, currentPath)
}
}
cmd/filesystem/main.go
package main
import (
"fmt"
"myapp/internal/filesystem"
)
func main() {
root := &filesystem.Directory{
Name: "root",
Entries: []filesystem.Entry{
&filesystem.File{Name: "readme.md", Size: 1024},
&filesystem.Directory{
Name: "src",
Entries: []filesystem.Entry{
&filesystem.File{Name: "main.go", Size: 2048},
&filesystem.File{Name: "util.go", Size: 512},
},
},
&filesystem.File{Name: "go.mod", Size: 256},
},
}
// List all entries
fmt.Println("listing:")
root.Accept(func(path string, isDir bool, size int64) {
if isDir {
fmt.Printf(" [DIR] %s\n", path)
} else {
fmt.Printf(" [FILE] %s (%d bytes)\n", path, size)
}
}, "")
// Calculate total size
var total int64
root.Accept(func(path string, isDir bool, size int64) {
total += size
}, "")
fmt.Printf("\ntotal size: %d bytes\n", total)
// Count files
var count int
root.Accept(func(path string, isDir bool, size int64) {
if !isDir {
count++
}
}, "")
fmt.Printf("file count: %d\n", count)
}
Considerations Link to heading
Example 1: Classic visitor with interface and Accept methods. Each node type implements Accept, and visitors implement a method per node type. Adding new operations is easy (new visitor), but adding new node types requires updating all visitors.
Example 2: Function-based visitor. When the operation is simple and uniform across types, a function is cleaner than an interface. The Accept method just calls the function with relevant data.
The Visitor pattern is verbose in Go compared to languages with method overloading. Consider it when you have a stable set of types but frequently add new operations. If you add new types often, the pattern becomes a burden since every visitor needs updating.
Verdict Link to heading
- Example 1: Code Smell (use only for complex AST-like structures with stable types)
- Example 2: Idiomatic