โ† Back to Blog

23 GoF Patterns in 23 Go Snippets

May 11, 2026 ยท 22 min read gopatternsjavacsharptutorial

The Gang of Four patterns are solutions to recurring design problems. Some languages need elaborate class hierarchies to implement them. Go doesn't โ€” first-class functions, implicit interfaces, and struct embedding absorb most of them into a few lines.

Here are all 23 patterns: what problem they solve, and the idiomatic Go implementation.

Just want the cheat sheet? Skip to the scorecard.


Creational Patterns

1. Singleton

What: Ensure a class has only one instance and provide a global point of access to it. Useful for shared resources like database connections or configuration.

Go gives you sync.Once โ€” one call, guaranteed, thread-safe, no locking gymnastics:

var db *DB
var once sync.Once

func GetDB() *DB {
    once.Do(func() { db = connectDB() })
    return db
}

One call, guaranteed. No locking gymnastics.

2. Factory Method

What: Define an interface for creating an object, but let the creation logic decide which concrete type to instantiate. Useful when the caller shouldn't know or care about the specific implementation.

In Go it's a function that returns an interface:

type Store interface { Save(data []byte) error }

func NewStore(kind string) Store {
    switch kind {
    case "s3":  return &S3Store{}
    case "disk": return &DiskStore{}
    }
    return &DiskStore{}
}

S3Store struct and DiskStore struct only need to implement a Save method with the same signature. That's all. No base class, no implements keyword, no registration.

3. Abstract Factory

What: Create families of related objects that must work together, without specifying their concrete types. Useful when swapping an entire product family (e.g., database drivers) as a unit.

Factory Method creates one object. Abstract Factory creates a family of related objects that must work together. Think database drivers: you need a connection, a transaction, and a query builder that all speak the same dialect. You don't want a Postgres connection paired with a MySQL query builder.

In Go, the factory is just a struct with function fields โ€” each field is a constructor for one product in the family:

type DBKit struct {
    Connect   func(dsn string) (Conn, error)
    NewTx     func(conn Conn) (Tx, error)
    NewQuery  func() QueryBuilder
}

func Postgres() DBKit {
    return DBKit{
        Connect:  pgConnect,
        NewTx:    pgBeginTx,
        NewQuery: func() QueryBuilder { return &PgQuery{} },
    }
}

Why a struct and not an interface? Because DBKit has no behavior โ€” it's just a bundle of constructors. An interface would force you to create a concrete type per family (PostgresKit struct, MySQLKit struct) with methods. A struct literal lets you assemble the family in one place with zero extra types.

The products (Conn, Tx, QueryBuilder) are still interfaces โ€” they need a contract. But the factory itself is just a function returning a struct. Swap Postgres() for MySQL() and the whole family travels together.

4. Builder

What: Construct a complex object step by step, allowing different configurations without telescoping constructors. Useful when an object has many optional parameters with sensible defaults.

You have a struct with 10 fields. Most have sensible defaults. The caller only wants to override two of them. Go uses functional options:

type Server struct {
    port    int
    timeout time.Duration
    tls     bool
}

type Option func(*Server)

func WithPort(p int) Option            { return func(s *Server) { s.port = p } }
func WithTimeout(d time.Duration) Option { return func(s *Server) { s.timeout = d } }
func WithTLS() Option                  { return func(s *Server) { s.tls = true } }

func NewServer(opts ...Option) *Server {
    s := &Server{port: 8080, timeout: 30 * time.Second} // defaults
    for _, o := range opts { o(s) }
    return s
}

// Usage: only override what you care about
srv := NewServer(WithPort(9090), WithTLS())

Each option is a function that mutates the struct. Defaults live in NewServer. Callers only pass what they want to change. You'll see this pattern in every serious Go library โ€” grpc.NewServer, zap.New, http.Server.

5. Prototype

What: Create new objects by copying an existing one, avoiding the cost of building from scratch. Useful for cloning templates or configurations that differ in only a few fields.

You have an object and you want a copy of it. Maybe a config template you clone and tweak per environment. In Go, structs are values. Assignment copies them:

type Config struct {
    Host    string
    Port    int
    Tags    []string
}

func (c Config) Clone() Config {
    clone := c                                  // copies Host and Port
    clone.Tags = append([]string{}, c.Tags...)  // deep copy the slice
    return clone
}

base := Config{Host: "localhost", Port: 5432, Tags: []string{"prod"}}
staging := base.Clone()
staging.Host = "staging-db"  // doesn't affect base

Scalar fields copy automatically. Slices and maps need explicit copies because they're reference types. That's the only thing to watch for.

Note: Clone() is purely a convention. There's no Cloneable interface in Go's standard library and no compiler enforcement. You could name it Copy() or Dup() โ€” nothing forces the name. It's just a community pattern that Clone() means "return a deep copy."


Structural Patterns

6. Adapter

What: Convert the interface of one type into another interface that clients expect. Useful when integrating legacy code or third-party libraries with incompatible APIs.

You have an old logger with a WriteLog method. Your new code expects a Logger interface with a Log method. They don't match. In Go, wrap it in a one-field struct and delegate:

type Logger interface { Log(msg string) }

type LogAdapter struct {
    legacy *externallib.LegacyLogger
}

func (a *LogAdapter) Log(msg string) { a.legacy.WriteLog(msg) }

No implements keyword, no interface registration. Just the right method signature. The compiler sees that LogAdapter has a Log(string) method and accepts it anywhere a Logger is expected.

If you own the type (it's in your package), it's even simpler โ€” just add the method directly:

type LegacyLogger struct{}
func (l *LegacyLogger) WriteLog(msg string) { fmt.Println(msg) }

// Add this one method and LegacyLogger satisfies Logger
func (l *LegacyLogger) Log(msg string) { l.WriteLog(msg) }

7. Bridge

What: Decouple an abstraction from its implementation so the two can vary independently. Useful when you have multiple dimensions of variation that would otherwise create a combinatorial explosion of types.

You have a notification system. Notifications can be urgent or normal (the abstraction). They can be sent via email, SMS, or Slack (the implementation). Without Bridge, you'd end up with UrgentEmail, UrgentSMS, UrgentSlack, NormalEmail, NormalSMS... every combination is a new type. It explodes.

Bridge separates the two dimensions so they vary independently. In Go it's a struct with an interface field:

type Sender interface {
    Send(to, message string) error
}

type EmailSender struct{}
func (e *EmailSender) Send(to, msg string) error { /* send email */ }

type SlackSender struct{}
func (s *SlackSender) Send(to, msg string) error { /* post to slack */ }

type Notification struct {
    sender  Sender
    urgent  bool
}

func (n *Notification) Notify(to, msg string) error {
    if n.urgent {
        msg = "๐Ÿšจ URGENT: " + msg
    }
    return n.sender.Send(to, msg)
}

Add a new sender? Implement Send. Add a new notification type? Change the struct. Neither side knows about the other.

Usage:

// Email + urgent
n := &Notification{sender: &EmailSender{}, urgent: true}
n.Notify("[email protected]", "Server is down")

// Slack + normal
n2 := &Notification{sender: &SlackSender{}, urgent: false}
n2.Notify("#ops", "Deploy finished")

Pick a Sender and a mode independently โ€” mix and match without a type for every combination.

8. Composite

What: Compose objects into tree structures so that individual objects and groups of objects are treated uniformly. Useful for hierarchical data like file systems, UI trees, or org charts.

You want to treat a single item and a group of items the same way. A file has a size. A directory has a size (sum of everything inside it). Your code shouldn't care which one it's dealing with โ€” just call .Size() and get the answer.

This comes up everywhere: file systems, org charts, UI component trees, permission groups. Anything hierarchical where a container behaves like its contents.

In Go it's an interface and a slice of that same interface:

type Node interface { Size() int64 }

type File struct{ size int64 }
func (f *File) Size() int64 { return f.size }

type Dir struct{ children []Node } // Node can be File or Dir
func (d *Dir) Size() int64 {
    var total int64
    for _, c := range d.children { total += c.Size() }
    return total
}

A Dir contains Nodes. A Node can be a File or another Dir. The recursion happens naturally. No abstract base class, no leaf vs composite distinction in the type system.

9. Decorator

What: Attach additional behavior to an object dynamically without modifying it. Useful for layering concerns like logging, authentication, or caching onto existing logic.

Go wraps a function with a function:

func WithLogging(h http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        log.Printf("%s %s", r.Method, r.URL.Path)
        h(w, r)
    }
}

Stack them: WithLogging(WithAuth(handler)).

Worth calling out: http.HandlerFunc is doing three patterns at once. A plain function with the right signature satisfies the http.Handler interface (Adapter). You pass different functions to change behavior (Strategy). You wrap them to add behavior (Decorator). One stdlib type, three patterns gone.

10. Facade

What: Provide a simplified interface to a complex subsystem. Useful when you want to hide internal complexity and give callers a single entry point.

Placing an order involves checking inventory, charging a card, and scheduling shipping. The caller shouldn't need to know about three different subsystems, their initialization order, or their error handling quirks. They just want PlaceOrder().

That's a facade. One simple function hiding complex coordination behind it.

In Go, the package system does this by default. Uppercase is your public API. Lowercase is hidden:

// package order โ€” consumers only see PlaceOrder

type inventoryClient struct{}
func (c *inventoryClient) Reserve(item string) error { /* check stock */ return nil }

type paymentClient struct{}
func (c *paymentClient) Charge(card string) error { /* charge card */ return nil }

type shippingClient struct{}
func (c *shippingClient) Schedule(item string) error { /* ship it */ return nil }

type Service struct {
    inventory *inventoryClient // lowercase = invisible outside this package
    payment   *paymentClient
    shipping  *shippingClient
}

func (s *Service) PlaceOrder(item, card string) error {
    if err := s.inventory.Reserve(item); err != nil { return err }
    if err := s.payment.Charge(card); err != nil { return err }
    return s.shipping.Schedule(item)
}

Consumers import order and call PlaceOrder. They never see inventory, payment, or shipping. Every well-designed Go package is already a facade โ€” you don't need a pattern name for it.

11. Flyweight

What: Share fine-grained objects to reduce memory usage when many similar objects exist. Useful when thousands of objects share common state that can be factored out.

You're rendering a document with 10,000 characters. Each character has a font, size, and color. If you create a separate object for every character with all its formatting data, you blow up memory. Most characters share the same formatting.

Flyweight says: share the common data, pass the unique data in from outside.

In Go it's a map that caches shared instances:

var fontCache sync.Map

type Font struct {
    Family string
    Size   int
    Bold   bool
}

func GetFont(family string, size int, bold bool) *Font {
    key := fmt.Sprintf("%s-%d-%v", family, size, bold)
    f := &Font{Family: family, Size: size, Bold: bold}
    actual, _ := fontCache.LoadOrStore(key, f)
    return actual.(*Font)
}

10,000 characters might only need 5 unique Font objects. sync.Map keeps it thread-safe. The characters hold a pointer to the shared font instead of their own copy.

Usage:

type Char struct {
    rune rune
    font *Font // shared, not copied, it is a pointer
}

// 10,000 characters, only a handful of Font allocations
doc := make([]Char, 10000)
for i := range doc {
    doc[i] = Char{rune: 'A', font: GetFont("Arial", 12, false)}
}

12. Proxy

What: Provide a surrogate or placeholder for another object to control access to it. Useful for adding caching, lazy loading, access control, or logging without modifying the original.

You have a database interface. Every query hits the network. Some queries return the same data every time. You want to add caching without changing the original implementation or the code that calls it.

A proxy sits in front of the real object, intercepts calls, and adds behavior (caching, logging, access control, lazy loading). The caller doesn't know it's talking to a proxy.

In Go, struct embedding forwards everything automatically. You only override what you intercept:

type DB interface {
    Query(sql string) ([]Row, error)
    Exec(sql string) error
}

type CachedDB struct {
    DB
    cache map[string][]Row // not thread-safe; add sync.RWMutex if queried concurrently
}

func NewCachedDB(real DB) *CachedDB {
    return &CachedDB{DB: real, cache: make(map[string][]Row)}
}

// Only Query is intercepted. Exec goes straight to the real DB.
func (c *CachedDB) Query(sql string) ([]Row, error) {
    if rows, ok := c.cache[sql]; ok { return rows, nil }
    rows, err := c.DB.Query(sql)
    if err == nil { c.cache[sql] = rows }
    return rows, err
}

If DB has 20 methods, you don't write 19 pass-throughs. Embedding handles them. You write code only for the one method you're intercepting. The constructor ensures the cache map is ready to use.


Behavioral Patterns

13. Strategy

What: Define a family of interchangeable algorithms and let the caller choose which one to use at runtime. Useful when the same operation needs different logic depending on context.

You have a checkout function. Sometimes it applies full price. Sometimes 50% off. Sometimes a custom discount based on the user's tier. The pricing logic changes, but the checkout flow stays the same.

In Go you pass a function:

type Pricer func(base float64) float64

func FullPrice(base float64) float64 { return base }
func HalfOff(base float64) float64   { return base * 0.5 }

func Checkout(items []Item, price Pricer) float64 {
    var total float64
    for _, i := range items { total += price(i.Base) }
    return total
}

// Usage
Checkout(cart, FullPrice)
Checkout(cart, HalfOff)
Checkout(cart, func(b float64) float64 { return b * 0.8 }) // inline 20% off

The strategy is just a function signature. You can pass a named function, an anonymous function, or a method. No interface, no class hierarchy, no factory to select it.

14. Observer

What: Define a one-to-many dependency so that when one object changes state, all dependents are notified automatically. Useful for event-driven systems where producers shouldn't be coupled to consumers.

A user signs up. You need to send a welcome email, create an analytics event, and provision their account. These are three separate concerns. The signup handler shouldn't know about all of them. It should just say "user created" and let interested parties react.

In Go, a slice of callbacks:

type EventBus struct {
    mu   sync.RWMutex // only needed if subscribing/emitting from multiple goroutines
    subs map[string][]func(any)
}

func (eb *EventBus) On(event string, fn func(any)) {
    eb.mu.Lock()
    defer eb.mu.Unlock()
    eb.subs[event] = append(eb.subs[event], fn)
}

func (eb *EventBus) Emit(event string, data any) {
    eb.mu.RLock()
    defer eb.mu.RUnlock()
    for _, fn := range eb.subs[event] { fn(data) }
}

// Usage
bus.On("user.created", sendWelcomeEmail)
bus.On("user.created", trackSignupEvent)
bus.On("user.created", provisionAccount)

bus.Emit("user.created", user)

The sync.RWMutex makes it safe to subscribe and emit from multiple goroutines. Caveat: if a callback calls On() during Emit(), it deadlocks. In production, copy the slice under the lock and call callbacks after releasing it.

If subscribers need to process events concurrently and at their own pace, channels are the alternative:

func fanOut(in <-chan Event, subs []chan<- Event) {
    for event := range in {
        for _, ch := range subs { ch <- event }
    }
}

Each subscriber gets its own channel.

Note: this sends synchronously โ€” a slow subscriber blocks the others. Add buffered channels or a select with timeout in production.

15. Command

What: Encapsulate a request as an object (or function) so you can queue, log, undo, or retry operations. Useful for task queues, undo stacks, and transactional pipelines.

You want to queue operations, execute them later, retry on failure, or undo them. A deploy pipeline: validate config, build image, push to registry, update service. Each step is independent. You want to run them in sequence, stop on failure, and maybe roll back.

In Go, functions go in a slice:

type Command func() error

func Execute(cmds []Command) error {
    for _, cmd := range cmds {
        if err := cmd(); err != nil { return err }
    }
    return nil
}

// Each step is just a function
steps := []Command{validateConfig, buildImage, pushToRegistry, updateService}
if err := Execute(steps); err != nil {
    rollback()
}

Need undo? Store each command with its reverse:

type Action struct {
    Do   func() error
    Undo func() error
}

Commands are data when they're functions. You can queue them, serialize them, replay them, batch them. No interface hierarchy needed.

16. Template Method

What: Define the skeleton of an algorithm in one place, letting subparts override specific steps. Useful for pipelines where the overall flow is fixed but individual steps vary.

You have an ETL pipeline. Every pipeline validates, transforms, and stores data. But the validation logic differs per data source. The transform logic differs per output format. The skeleton is the same, the steps change.

In Go, the steps are function fields on a struct:

type Pipeline struct {
    Validate  func(data []byte) error
    Transform func(data []byte) ([]byte, error)
    Store     func(data []byte) error
}

func (p *Pipeline) Run(data []byte) error {
    if err := p.Validate(data); err != nil { return err }
    out, err := p.Transform(data)
    if err != nil { return err }
    return p.Store(out)
}

// CSV pipeline
csvPipeline := &Pipeline{
    Validate:  validateCSV,
    Transform: csvToJSON,
    Store:     writeToS3,
}

// XML pipeline โ€” same skeleton, different steps
xmlPipeline := &Pipeline{
    Validate:  validateXML,
    Transform: xmlToJSON,
    Store:     writeToDB,
}

Same flow, different behavior. No inheritance, no abstract classes. Swap the functions.

17. Iterator

What: Provide a way to sequentially access elements of a collection without exposing its underlying structure. Useful for custom data structures that should feel like built-in collections to callers.

You have a custom data structure โ€” a tree, a linked list, a paginated API response. You want to loop over its elements with for range like you would a slice.

The simplest iterator is a slice. You already use for range on it every day. But what about custom data structures?

Before Go 1.23, you'd return a slice (simple but allocates) or use a channel (concurrent but heavy). Since Go 1.23, you can make any data structure work with for range by returning a special function:

// Iterate over a slice in reverse
func Backward(s []string) iter.Seq[string] {
    return func(emit func(string) bool) {
        for i := len(s) - 1; i >= 0; i-- {
            if !emit(s[i]) {
                return // caller used break
            }
        }
    }
}

// Caller writes a normal loop
for val := range Backward([]string{"a", "b", "c"}) {
    fmt.Println(val) // prints c, b, a
}

The function receives an emit callback from for range โ€” call it for each element, stop if it returns false (the caller used break).

The tree version is the same idea โ€” walk recursively, call emit at each node:

func (t *Tree[V]) All() iter.Seq[V] {
    return func(emit func(V) bool) {
        t.walk(t.root, emit) // visits each node, calls emit(node.Value)
    }
}

for val := range tree.All() {
    fmt.Println(val)
}

The caller writes a for range loop. They don't know if it's a slice, a tree, or a database cursor behind it.

18. State

What: Allow an object to change its behavior when its internal state changes, appearing to change its type. Useful for finite state machines like connections, workflows, or game entities.

A TCP connection can be in different states: listening, open, closed. The behavior of Read() and Write() depends on which state it's in. In the closed state, Write() should return an error. In the open state, it should send data.

In Go, a state is a function that returns the next state:

type State func(event string) State

This is a recursive type โ€” a function whose return type is itself. Each state is a function that, given an event, returns whichever function should handle the next event.

func Idle(event string) State {
    if event == "connect" { return Connected }
    return Idle
}

func Connected(event string) State {
    switch event {
    case "send":       return Connected // stay
    case "disconnect": return Idle
    }
    return Connected
}

// Run the state machine
current := Idle
current = current("connect")  // โ†’ Connected
current = current("send")     // โ†’ Connected
current = current("disconnect") // โ†’ Idle

The state machine is the type signature. Each state knows its transitions. No class hierarchy, no context object.

19. Chain of Responsibility

What: Pass a request along a chain of handlers, where each handler decides whether to process it or pass it along. Useful for middleware stacks, validation pipelines, and event processing.

An HTTP request comes in. It needs authentication checked, then rate limiting, then logging, then the actual handler. Each step decides whether to pass the request along or stop it.

In Go this is called middleware. Every Go developer has written this:

type Middleware func(http.Handler) http.Handler

func WithAuth(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if !isAuthenticated(r) {
            http.Error(w, "unauthorized", 401)
            return // stop the chain
        }
        next.ServeHTTP(w, r) // pass to next
    })
}

func Chain(h http.Handler, mws ...Middleware) http.Handler {
    for i := len(mws) - 1; i >= 0; i-- {
        h = mws[i](h)
    }
    return h
}

// Usage
handler := Chain(myHandler, WithAuth, WithRateLimit, WithLogging)

If you've written Go HTTP middleware, you've implemented Chain of Responsibility. You just didn't need to name it.

20. Mediator

What: Define an object that encapsulates how a set of objects interact, preventing direct references between them. Useful for chat rooms, event dispatchers, or any system where many-to-many communication needs a central coordinator.

You have a chat system. Users shouldn't hold references to every other user. They shouldn't know who's online or how messages get routed. They just send to the room and the room figures it out.

In Go it's a struct that owns the channels:

type ChatRoom struct {
    mu    sync.RWMutex // only needed if Join/Send called from multiple goroutines
    users map[string]chan string
}

func NewChatRoom() *ChatRoom {
    return &ChatRoom{users: make(map[string]chan string)}
}

func (c *ChatRoom) Join(name string) <-chan string {
    ch := make(chan string, 10)
    c.mu.Lock()
    c.users[name] = ch
    c.mu.Unlock()
    return ch
}

func (c *ChatRoom) Send(from, to, msg string) {
    c.mu.RLock()
    ch, ok := c.users[to]
    c.mu.RUnlock()
    if ok {
        select {
        case ch <- fmt.Sprintf("[%s]: %s", from, msg):
        default: // drop message if receiver is full
        }
    }
}

Users know the room. The room knows the users. Users don't know each other. That's the whole pattern.

21. Memento

What: Capture an object's internal state so it can be restored later without exposing its implementation details. Useful for undo/redo, checkpoints, and transactional rollbacks.

You're building a text editor. The user types, deletes, types more. They hit undo. You need to restore the previous state without exposing the editor's internals.

In Go, value semantics make this trivial. Copy the struct, push it to a slice:

type Editor struct{ content string }

func (e *Editor) Snapshot() string { return e.content }
func (e *Editor) Restore(s string) { e.content = s }

// Undo stack is just a slice
var history []string

history = append(history, editor.Snapshot()) // save
editor.Restore(history[len(history)-1])      // undo

No Memento class. No Caretaker class. A slice is your undo stack.

22. Visitor

What: Define a new operation on a set of types without modifying those types. Useful for AST traversal, serialization, or any case where you add operations to a closed type hierarchy.

You have an AST (abstract syntax tree) with different node types: numbers, addition, multiplication. You want to add operations (evaluate, print, optimize) without modifying the node types themselves.

In Go, type switch:

func Evaluate(node Node) float64 {
    switch n := node.(type) {
    case *Number: return n.Value
    case *Add:    return Evaluate(n.Left) + Evaluate(n.Right)
    case *Mul:    return Evaluate(n.Left) * Evaluate(n.Right)
    }
    return 0
}

func Print(node Node) string {
    switch n := node.(type) {
    case *Number: return fmt.Sprintf("%g", n.Value)
    case *Add:    return fmt.Sprintf("(%s + %s)", Print(n.Left), Print(n.Right))
    case *Mul:    return fmt.Sprintf("(%s * %s)", Print(n.Left), Print(n.Right))
    }
    return ""
}

New operation? Write a new function with a type switch. No changes to existing node types. No double dispatch.

23. Interpreter

What: Define a grammar for a simple language and an interpreter to evaluate sentences in it. Useful for rule engines, query builders, and DSLs where users express logic declaratively.

You want users to express rules or queries in a mini-language. "Give me electronics over $50" or "retry 3 times with exponential backoff." You need to parse that intent and execute it.

In Go, functions compose:

type Filter func(item Item) bool

func PriceAbove(min float64) Filter {
    return func(i Item) bool { return i.Price > min }
}

func InCategory(cat string) Filter {
    return func(i Item) bool { return i.Category == cat }
}

func Every(filters ...Filter) Filter {
    return func(i Item) bool {
        for _, f := range filters {
            if !f(i) { return false }
        }
        return true
    }
}

// Build a query by composing functions
expensive := PriceAbove(50)
electronics := InCategory("electronics")
query := Every(expensive, electronics)

// Use it
for _, item := range catalog {
    if query(item) { fmt.Println(item.Name) }
}

Each function is a grammar rule. Every combines them. You build complex queries by composing simple functions. No expression tree classes, no parser hierarchy.


The Scorecard

# Pattern What it solves Go
Creational
1 Singleton One global instance sync.Once
2 Factory Method Decouple object creation from usage Function returning interface
3 Abstract Factory Create families of related objects Struct of factory functions
4 Builder Complex object with many optional params Functional options (...Option)
5 Prototype Clone an existing object Value copy
Structural
6 Adapter Make incompatible interfaces work together Implicit interface satisfaction
7 Bridge Separate abstraction from implementation Struct with interface field
8 Composite Treat individual and group uniformly Interface + slice of interface
9 Decorator Add behavior without modifying original Middleware func(H) H
10 Facade Simplify a complex subsystem Package exports
11 Flyweight Share objects to save memory Shared instance cache (sync.Map)
12 Proxy Control access to another object Embedding + method override
Behavioral
13 Strategy Swap algorithms at runtime Pass a func
14 Observer Notify dependents of state changes Channels or callback slice
15 Command Encapsulate operations as data func in a slice
16 Template Method Fixed skeleton, variable steps Struct with func fields
17 Iterator Sequential access to elements range / iter.Seq
18 State Behavior changes with internal state func returning next func
19 Chain of Responsibility Pass request through handler chain Middleware chain
20 Mediator Centralize object interactions Struct with channels
21 Memento Capture and restore state Struct copy
22 Visitor Add operations without modifying types Type switch
23 Interpreter Evaluate a mini-language Composable functions

What This Means

Go didn't remove design patterns. It absorbed them into the language.

First-class functions kill every pattern that exists to pass behavior around. Strategy, Command, Observer, Template Method โ€” gone. Implicit interfaces kill every pattern that exists to make types compatible. Adapter, Bridge โ€” gone. Composition via embedding kills every pattern that relies on inheritance. Proxy, Decorator โ€” gone.

You don't need to memorize 23 patterns. You need to understand three language features: functions as values, interfaces without declarations, and struct embedding. The rest follows.

Not all patterns disappear completely. Composite is still a recursive tree worth naming. State machines are still state machines. Middleware is still an architectural decision. But the implementation drops from "three classes and an interface" to "five lines and a function signature."

The patterns are still here. They're just not ceremonies anymore.

If you're picking up Go and want to write it idiomatically from day one, I have two free courses on ByteLearn โ€” no signup wall:

  • Go Essentials โ€” interfaces, composition, and the language features that replace these patterns. Start here if you're learning Go.
  • Go Concurrency Patterns โ€” pipelines, worker pools, channels, and real-world concurrent design. Start here if you already know Go and want to master concurrency.

Got thoughts on this post?

I'd love to hear from you. Reach out on any of these:

Want to learn by doing?

ByteLearn.dev has free courses with interactive quizzes for developers.

Browse courses โ†’
ยฉ 2026 ByteLearn.dev. Free courses for developers. ยท Privacy