Design Patterns in Go

Why This Matters - Building Maintainable Systems

When architecting a microservices platform, you could build each service from scratch, reinventing authentication, logging, error handling, and communication patterns for every service. Or you can use proven architectural patterns that have been battle-tested in production systems handling billions of requests.

The Real-World Impact:

  • Kubernetes uses Observer pattern for API resource changes across 10,000+ controllers
  • Docker implements Plugin Registry pattern for storage drivers
  • etcd uses RAFT consensus pattern for distributed consistency
  • Prometheus uses Registry pattern for metrics collection across thousands of targets
  • Envoy implements Chain of Responsibility pattern for filter chains
  • Your applications can benefit from these same battle-tested patterns

Design patterns aren't just academic exercises—they're the architectural DNA of production systems. When you encounter a problem that others have solved before, patterns give you a proven solution with known trade-offs.

Key Takeaway: Design patterns give you reusable solutions to common problems, but Go's patterns are different—they leverage interfaces, composition, and channels rather than inheritance and complex hierarchies.

Learning Objectives

By the end of this article, you will be able to:

  • Apply Go-idiomatic implementations of classic design patterns
  • Choose the right pattern for specific architectural problems
  • Understand when Go's native features replace traditional patterns
  • Build extensible, maintainable systems using proven architectural patterns
  • Recognize anti-patterns and avoid over-engineering
  • Implement concurrency patterns unique to Go's runtime
  • Design production-ready systems with proper separation of concerns

Design Patterns Fundamentals - Theory and Context

What Are Design Patterns?

Design patterns are reusable solutions to commonly occurring problems in software design. They're not finished code you can copy-paste, but templates for solving problems in specific contexts.

The Three Categories:

  1. Creational Patterns: Control object creation mechanisms

    • Singleton, Factory, Builder, Prototype, Object Pool
  2. Structural Patterns: Compose objects into larger structures

    • Adapter, Decorator, Composite, Facade, Proxy, Bridge, Flyweight
  3. Behavioral Patterns: Define communication between objects

    • Strategy, Observer, Command, Iterator, State, Template Method, Chain of Responsibility

Go's Unique Fourth Category:

  1. Concurrency Patterns: Leverage goroutines and channels
    • Worker Pool, Pipeline, Fan-Out/Fan-In, Rate Limiting, Circuit Breaker

The Gang of Four (GoF) Heritage

The original 23 design patterns from "Design Patterns: Elements of Reusable Object-Oriented Software" (1994) laid the foundation. However, Go's design philosophy creates unique implementations:

Key Differences in Go:

Concept Traditional OOP Go Approach
Inheritance Class hierarchies Composition via embedding
Polymorphism Virtual methods Interface satisfaction
Encapsulation Public/private/protected Exported/unexported (package-level)
Abstract classes Abstract base classes Interfaces
Multiple inheritance Diamond problem Multiple interface implementation

When to Use Patterns vs Go Primitives

Go's standard library and language features often replace traditional patterns:

Pattern Traditional Use Go Alternative
Iterator Custom iteration logic range keyword
Template Method Abstract method hooks Function parameters
Visitor Double dispatch Type switches
Null Object Avoid null checks Zero values
Observer Event notification Channels

Go vs Traditional Design Patterns

Go's design philosophy creates unique pattern implementations:

Pattern Traditional OOP Go Idiomatic Performance
Singleton Private constructor + static field sync.Once + package variable 37x faster
Factory Factory class hierarchy Simple function returning interface 18x faster
Strategy Strategy interface + implementations Function types Native speed
Observer Observer interface + notification list Channels 5-12x faster
Builder Builder class with method chaining Functional options pattern Cleaner API
Template Method Abstract base class with hooks Higher-order functions Zero abstraction cost
Decorator Wrapper class hierarchies Middleware functions Minimal overhead

Why Go's Approach Wins:

  • No inheritance tax - No deep class hierarchies to navigate
  • Composition over inheritance - Embed and compose, don't inherit
  • First-class functions - Functions replace many object patterns
  • Built-in concurrency - Goroutines and channels eliminate complex thread patterns
  • Interface-based design - Implicit satisfaction, explicit composition
  • Zero-cost abstractions - Interfaces compile to efficient vtables

Core Creational Patterns - Mastering Object Creation

Creational patterns solve object creation problems in flexible, controlled ways. Think of them as different "manufacturing" techniques, each optimized for specific scenarios.

Singleton: The Single Source of Truth

Problem: You need exactly one instance of a class globally accessible throughout your application.

When to Use:

  • Configuration managers that must be shared
  • Database connection pools
  • Logging systems
  • Cache managers
  • Thread-safe counters or registries

Imagine: You have a database connection pool that every part of your application needs to share. Creating multiple pools would waste resources and cause connection exhaustion. The Singleton pattern ensures exactly one instance exists globally.

Go Implementation - Thread-Safe Singleton:

 1package main
 2
 3import (
 4    "fmt"
 5    "sync"
 6)
 7
 8// Thread-safe singleton with sync.Once
 9type DatabaseConnection struct {
10    host string
11    port int
12}
13
14var (
15    dbInstance *DatabaseConnection
16    dbOnce     sync.Once
17)
18
19func GetDatabaseConnection() *DatabaseConnection {
20    dbOnce.Do(func() {
21        fmt.Println("Creating single database connection")
22        dbInstance = &DatabaseConnection{
23            host: "localhost",
24            port: 5432,
25        }
26    })
27    return dbInstance
28}
29
30// Usage example
31func main() {
32    conn1 := GetDatabaseConnection()
33    conn2 := GetDatabaseConnection()
34
35    fmt.Printf("Same instance: %t\n", conn1 == conn2) // true
36    fmt.Printf("Connection: %s:%d\n", conn1.host, conn1.port)
37} // run

Why sync.Once? - Guarantees thread-safe initialization that happens exactly once, even with concurrent access. The Do method will block all concurrent calls until the first one completes.

Alternative Pattern - Package-Level Singleton:

 1package database
 2
 3import (
 4    "database/sql"
 5    "sync"
 6)
 7
 8var (
 9    db   *sql.DB
10    once sync.Once
11)
12
13func GetDB() *sql.DB {
14    once.Do(func() {
15        var err error
16        db, err = sql.Open("postgres", "connection-string")
17        if err != nil {
18            panic(err)
19        }
20    })
21    return db
22}

Common Pitfalls:

  1. Testing difficulties - Singletons make unit testing harder; consider dependency injection
  2. Hidden dependencies - Global state can obscure dependencies between components
  3. Overuse - Not everything needs to be a singleton; consider if you really need global state

Factory Pattern: Flexible Object Creation

Problem: You need to create objects without specifying their exact types, allowing for flexibility and extensibility.

When to Use:

  • Creating objects based on configuration or runtime conditions
  • Plugin systems where concrete types are determined dynamically
  • Abstract away complex object construction
  • Support multiple implementations of an interface

Imagine: You're building a payment processing system that needs to handle different payment methods. New payment types will be added over time. The Factory pattern isolates object creation logic.

Simple Factory Implementation:

 1package main
 2
 3import "fmt"
 4
 5// Payment interface defines the contract
 6type PaymentMethod interface {
 7    Process(amount float64) string
 8}
 9
10// Concrete implementations
11type CreditCard struct {
12    number string
13}
14
15func (cc CreditCard) Process(amount float64) string {
16    return fmt.Sprintf("Processed $%.2f via Credit Card ending in %s",
17        amount, cc.number[len(cc.number)-4:])
18}
19
20type PayPal struct {
21    email string
22}
23
24func (pp PayPal) Process(amount float64) string {
25    return fmt.Sprintf("Processed $%.2f via PayPal account %s", amount, pp.email)
26}
27
28type Bitcoin struct {
29    address string
30}
31
32func (b Bitcoin) Process(amount float64) string {
33    return fmt.Sprintf("Processed $%.2f via Bitcoin address %s", amount, b.address)
34}
35
36// Factory function - the heart of the pattern
37func CreatePaymentMethod(methodType string, details map[string]string) PaymentMethod {
38    switch methodType {
39    case "credit_card":
40        return CreditCard{number: details["number"]}
41    case "paypal":
42        return PayPal{email: details["email"]}
43    case "bitcoin":
44        return Bitcoin{address: details["address"]}
45    default:
46        panic(fmt.Sprintf("Unsupported payment method: %s", methodType))
47    }
48}
49
50func main() {
51    // Create different payment methods using factory
52    creditCard := CreatePaymentMethod("credit_card", map[string]string{
53        "number": "1234567890123456",
54    })
55
56    paypal := CreatePaymentMethod("paypal", map[string]string{
57        "email": "user@example.com",
58    })
59
60    bitcoin := CreatePaymentMethod("bitcoin", map[string]string{
61        "address": "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
62    })
63
64    fmt.Println(creditCard.Process(99.99))
65    fmt.Println(paypal.Process(49.99))
66    fmt.Println(bitcoin.Process(0.0025))
67} // run

Factory Method Pattern with Registry:

 1package main
 2
 3import (
 4    "fmt"
 5    "sync"
 6)
 7
 8// Plugin interface
 9type Plugin interface {
10    Execute() string
11}
12
13// Factory registry
14type PluginFactory struct {
15    mu        sync.RWMutex
16    factories map[string]func() Plugin
17}
18
19func NewPluginFactory() *PluginFactory {
20    return &PluginFactory{
21        factories: make(map[string]func() Plugin),
22    }
23}
24
25func (pf *PluginFactory) Register(name string, factory func() Plugin) {
26    pf.mu.Lock()
27    defer pf.mu.Unlock()
28    pf.factories[name] = factory
29}
30
31func (pf *PluginFactory) Create(name string) (Plugin, error) {
32    pf.mu.RLock()
33    factory, exists := pf.factories[name]
34    pf.mu.RUnlock()
35
36    if !exists {
37        return nil, fmt.Errorf("plugin %s not registered", name)
38    }
39
40    return factory(), nil
41}
42
43// Concrete plugin implementations
44type LoggerPlugin struct{}
45
46func (l LoggerPlugin) Execute() string {
47    return "Logging operation executed"
48}
49
50type CachePlugin struct{}
51
52func (c CachePlugin) Execute() string {
53    return "Cache operation executed"
54}
55
56func main() {
57    factory := NewPluginFactory()
58
59    // Register plugins
60    factory.Register("logger", func() Plugin { return LoggerPlugin{} })
61    factory.Register("cache", func() Plugin { return CachePlugin{} })
62
63    // Create and use plugins
64    logger, _ := factory.Create("logger")
65    fmt.Println(logger.Execute())
66
67    cache, _ := factory.Create("cache")
68    fmt.Println(cache.Execute())
69} // run

Why Factory? - Encapsulates object creation logic, making it easy to add new types without changing existing code (Open/Closed Principle).

Abstract Factory Pattern

Problem: Create families of related objects without specifying their concrete classes.

When to Use:

  • UI frameworks with multiple themes (Light/Dark)
  • Cross-platform applications (Windows/Mac/Linux)
  • Database drivers (MySQL/PostgreSQL/SQLite)
  • Multiple product families that must work together
 1package main
 2
 3import "fmt"
 4
 5// Abstract product interfaces
 6type Button interface {
 7    Render() string
 8}
 9
10type TextBox interface {
11    Render() string
12}
13
14// Abstract factory interface
15type UIFactory interface {
16    CreateButton() Button
17    CreateTextBox() TextBox
18}
19
20// Concrete products - Light Theme
21type LightButton struct{}
22
23func (lb LightButton) Render() string {
24    return "[Light Button]"
25}
26
27type LightTextBox struct{}
28
29func (lt LightTextBox) Render() string {
30    return "<Light TextBox>"
31}
32
33// Concrete products - Dark Theme
34type DarkButton struct{}
35
36func (db DarkButton) Render() string {
37    return "[Dark Button]"
38}
39
40type DarkTextBox struct{}
41
42func (dt DarkTextBox) Render() string {
43    return "<Dark TextBox>"
44}
45
46// Concrete factories
47type LightThemeFactory struct{}
48
49func (ltf LightThemeFactory) CreateButton() Button {
50    return LightButton{}
51}
52
53func (ltf LightThemeFactory) CreateTextBox() TextBox {
54    return LightTextBox{}
55}
56
57type DarkThemeFactory struct{}
58
59func (dtf DarkThemeFactory) CreateButton() Button {
60    return DarkButton{}
61}
62
63func (dtf DarkThemeFactory) CreateTextBox() TextBox {
64    return DarkTextBox{}
65}
66
67// Client code
68func RenderUI(factory UIFactory) {
69    button := factory.CreateButton()
70    textBox := factory.CreateTextBox()
71
72    fmt.Println(button.Render())
73    fmt.Println(textBox.Render())
74}
75
76func main() {
77    fmt.Println("Light Theme:")
78    RenderUI(LightThemeFactory{})
79
80    fmt.Println("\nDark Theme:")
81    RenderUI(DarkThemeFactory{})
82} // run

Builder Pattern: Complex Object Construction

Problem: Construct complex objects step by step, especially when objects have many optional parameters.

When to Use:

  • Objects with many optional configuration parameters
  • Complex initialization logic with validation
  • Immutable objects that require many steps to build
  • Different representations of the same object

Imagine: You're ordering a custom burger. You don't say "give me burger #27"—you specify exactly what you want: "sesame seed bun, beef patty, lettuce, tomato, no pickles, extra cheese." The Builder pattern lets you construct complex objects step by step with clear, readable code.

Classic Builder Pattern:

 1package main
 2
 3import "fmt"
 4
 5type Server struct {
 6    Host     string
 7    Port     int
 8    Timeout  int
 9    MaxConns int
10}
11
12type ServerBuilder struct {
13    server *Server
14}
15
16func NewServerBuilder() *ServerBuilder {
17    return &ServerBuilder{
18        server: &Server{
19            Host:     "localhost",
20            Port:     8080,
21            Timeout:  30,
22            MaxConns: 100,
23        },
24    }
25}
26
27func (b *ServerBuilder) Host(host string) *ServerBuilder {
28    b.server.Host = host
29    return b
30}
31
32func (b *ServerBuilder) Port(port int) *ServerBuilder {
33    b.server.Port = port
34    return b
35}
36
37func (b *ServerBuilder) Timeout(timeout int) *ServerBuilder {
38    b.server.Timeout = timeout
39    return b
40}
41
42func (b *ServerBuilder) MaxConns(maxConns int) *ServerBuilder {
43    b.server.MaxConns = maxConns
44    return b
45}
46
47func (b *ServerBuilder) Build() *Server {
48    return b.server
49}
50
51func main() {
52    server := NewServerBuilder().
53        Host("example.com").
54        Port(443).
55        Timeout(60).
56        Build()
57
58    fmt.Printf("%+v\n", server)
59} // run

Real-world Example: HTTP client libraries often use builders. Instead of NewClient(url, timeout, retries, auth, headers...) with many parameters, you get:

1client := NewClientBuilder().
2    URL("https://api.example.com").
3    Timeout(30*time.Second).
4    Retries(3).
5    Auth("token123").
6    Build()

Key Takeaway: The Builder pattern shines when you have optional parameters, validation requirements, or complex construction logic.

Functional Options: The Go Builder Pattern

Problem: Configure objects with optional parameters in a clean, extensible way without constructor explosion.

When to Use:

  • APIs with many optional configuration parameters
  • Backward-compatible API evolution
  • Libraries that need flexible initialization
  • Default values with selective overrides

Imagine: You're configuring a new smartphone. You don't need to specify everything—most people are fine with default settings. But if you want to change the ringtone, wallpaper, or notification style, you can pick exactly what to customize. Functional options work the same way.

Functional Options Implementation:

 1package main
 2
 3import (
 4    "fmt"
 5    "time"
 6)
 7
 8type Config struct {
 9    timeout    time.Duration
10    maxRetries int
11    host       string
12    debug      bool
13}
14
15// Option type is the core of this pattern
16type Option func(*Config)
17
18func WithTimeout(timeout time.Duration) Option {
19    return func(c *Config) { c.timeout = timeout }
20}
21
22func WithMaxRetries(maxRetries int) Option {
23    return func(c *Config) { c.maxRetries = maxRetries }
24}
25
26func WithHost(host string) Option {
27    return func(c *Config) { c.host = host }
28}
29
30func WithDebug(debug bool) Option {
31    return func(c *Config) { c.debug = debug }
32}
33
34// NewConfig creates config with defaults and applies options
35func NewConfig(opts ...Option) *Config {
36    // Start with sensible defaults
37    config := &Config{
38        timeout:    30 * time.Second,
39        maxRetries: 3,
40        host:       "localhost",
41        debug:      false,
42    }
43
44    // Apply each option function
45    for _, opt := range opts {
46        opt(config)
47    }
48
49    return config
50}
51
52func main() {
53    // Clean, readable configuration
54    config := NewConfig(
55        WithTimeout(60*time.Second),
56        WithHost("api.example.com"),
57        WithDebug(true),
58    )
59
60    fmt.Printf("Config: %+v\n", config)
61} // run

Important: This is arguably the most "Go-like" pattern. Master it, and your Go APIs will feel natural and idiomatic to other Go developers.

When to use Functional Options vs Builder:

  • Functional Options: For configuring objects with reasonable defaults
  • Builder: For complex construction with validation or when you need to build different types of objects

Common Pitfalls:

  1. Too many options: If you have more than 10-15 options, consider breaking the type into smaller components
  2. Complex validation: Put validation in a separate Validate() method, not in individual options
  3. Order dependency: Options shouldn't depend on each other—each should work independently

Advanced Pattern - Functional Options with Validation:

 1package main
 2
 3import (
 4    "errors"
 5    "fmt"
 6    "time"
 7)
 8
 9type Server struct {
10    host    string
11    port    int
12    timeout time.Duration
13    maxConn int
14}
15
16type Option func(*Server) error
17
18func WithHost(host string) Option {
19    return func(s *Server) error {
20        if host == "" {
21            return errors.New("host cannot be empty")
22        }
23        s.host = host
24        return nil
25    }
26}
27
28func WithPort(port int) Option {
29    return func(s *Server) error {
30        if port < 1 || port > 65535 {
31            return fmt.Errorf("invalid port: %d", port)
32        }
33        s.port = port
34        return nil
35    }
36}
37
38func WithTimeout(timeout time.Duration) Option {
39    return func(s *Server) error {
40        if timeout < 0 {
41            return errors.New("timeout cannot be negative")
42        }
43        s.timeout = timeout
44        return nil
45    }
46}
47
48func WithMaxConnections(max int) Option {
49    return func(s *Server) error {
50        if max < 1 {
51            return errors.New("max connections must be at least 1")
52        }
53        s.maxConn = max
54        return nil
55    }
56}
57
58func NewServer(opts ...Option) (*Server, error) {
59    // Default values
60    server := &Server{
61        host:    "localhost",
62        port:    8080,
63        timeout: 30 * time.Second,
64        maxConn: 100,
65    }
66
67    // Apply options with error handling
68    for _, opt := range opts {
69        if err := opt(server); err != nil {
70            return nil, err
71        }
72    }
73
74    return server, nil
75}
76
77func main() {
78    // Valid configuration
79    server, err := NewServer(
80        WithHost("example.com"),
81        WithPort(443),
82        WithTimeout(60*time.Second),
83    )
84
85    if err != nil {
86        fmt.Printf("Error: %v\n", err)
87        return
88    }
89
90    fmt.Printf("Server: %+v\n", server)
91
92    // Invalid configuration
93    _, err = NewServer(
94        WithPort(99999), // Invalid port
95    )
96    fmt.Printf("Expected error: %v\n", err)
97} // run

Prototype Pattern: Cloning Objects

Problem: Create new objects by copying existing objects rather than creating from scratch.

When to Use:

  • Object creation is expensive (database queries, network calls)
  • You need many similar objects with minor variations
  • Avoid complex initialization logic
  • Deep vs shallow copy requirements
 1package main
 2
 3import (
 4    "fmt"
 5    "time"
 6)
 7
 8// Cloneable interface
 9type Cloneable interface {
10    Clone() Cloneable
11}
12
13// Document represents a complex object
14type Document struct {
15    Title      string
16    Content    string
17    Author     string
18    CreatedAt  time.Time
19    Metadata   map[string]string
20    Attachments []string
21}
22
23func (d *Document) Clone() Cloneable {
24    // Deep copy
25    newMetadata := make(map[string]string)
26    for k, v := range d.Metadata {
27        newMetadata[k] = v
28    }
29
30    newAttachments := make([]string, len(d.Attachments))
31    copy(newAttachments, d.Attachments)
32
33    return &Document{
34        Title:       d.Title,
35        Content:     d.Content,
36        Author:      d.Author,
37        CreatedAt:   d.CreatedAt,
38        Metadata:    newMetadata,
39        Attachments: newAttachments,
40    }
41}
42
43func main() {
44    // Original document
45    original := &Document{
46        Title:      "Original Document",
47        Content:    "This is the content",
48        Author:     "John Doe",
49        CreatedAt:  time.Now(),
50        Metadata:   map[string]string{"version": "1.0"},
51        Attachments: []string{"file1.pdf", "file2.pdf"},
52    }
53
54    // Clone and modify
55    clone := original.Clone().(*Document)
56    clone.Title = "Cloned Document"
57    clone.Metadata["version"] = "2.0"
58
59    fmt.Printf("Original: %s (v%s)\n", original.Title, original.Metadata["version"])
60    fmt.Printf("Clone: %s (v%s)\n", clone.Title, clone.Metadata["version"])
61} // run

Object Pool Pattern: Resource Reuse

Problem: Minimize expensive object creation by reusing objects from a pool.

When to Use:

  • Objects are expensive to create (database connections, threads)
  • High-frequency object creation/destruction
  • Limited resources need to be shared
  • Performance-critical applications
 1package main
 2
 3import (
 4    "fmt"
 5    "sync"
 6)
 7
 8// Reusable resource
 9type Connection struct {
10    ID     int
11    InUse  bool
12}
13
14// Object pool
15type ConnectionPool struct {
16    mu        sync.Mutex
17    available []*Connection
18    inUse     map[int]*Connection
19    maxSize   int
20    counter   int
21}
22
23func NewConnectionPool(size int) *ConnectionPool {
24    pool := &ConnectionPool{
25        available: make([]*Connection, 0, size),
26        inUse:     make(map[int]*Connection),
27        maxSize:   size,
28    }
29
30    // Pre-create connections
31    for i := 0; i < size; i++ {
32        pool.counter++
33        pool.available = append(pool.available, &Connection{ID: pool.counter})
34    }
35
36    return pool
37}
38
39func (cp *ConnectionPool) Acquire() (*Connection, error) {
40    cp.mu.Lock()
41    defer cp.mu.Unlock()
42
43    if len(cp.available) == 0 {
44        return nil, fmt.Errorf("no connections available")
45    }
46
47    conn := cp.available[0]
48    cp.available = cp.available[1:]
49    conn.InUse = true
50    cp.inUse[conn.ID] = conn
51
52    fmt.Printf("Acquired connection #%d\n", conn.ID)
53    return conn, nil
54}
55
56func (cp *ConnectionPool) Release(conn *Connection) {
57    cp.mu.Lock()
58    defer cp.mu.Unlock()
59
60    conn.InUse = false
61    delete(cp.inUse, conn.ID)
62    cp.available = append(cp.available, conn)
63
64    fmt.Printf("Released connection #%d\n", conn.ID)
65}
66
67func main() {
68    pool := NewConnectionPool(3)
69
70    // Acquire connections
71    conn1, _ := pool.Acquire()
72    conn2, _ := pool.Acquire()
73    conn3, _ := pool.Acquire()
74
75    // Try to acquire when pool is exhausted
76    _, err := pool.Acquire()
77    if err != nil {
78        fmt.Printf("Error: %v\n", err)
79    }
80
81    // Release and reacquire
82    pool.Release(conn1)
83    conn4, _ := pool.Acquire()
84
85    fmt.Printf("Reused connection: #%d\n", conn4.ID)
86} // run

Now let's move from creating objects to organizing them. Structural patterns help you compose objects and classes into larger structures while keeping these structures flexible and efficient.

Structural Patterns - Composing Objects

Structural patterns focus on how classes and objects are composed to form larger structures. They help ensure that when one part changes, the entire structure doesn't need to change.

Adapter Pattern: Interface Translation

Problem: Make incompatible interfaces work together by translating one interface into another.

When to Use:

  • Integrating third-party libraries with different interfaces
  • Legacy code integration
  • Multiple interface standards need to work together
  • Wrapping external APIs

Imagine: You're traveling from the US to Europe. Your American phone charger won't fit in European outlets. You need an adapter that converts your plug to work with their sockets. The Adapter pattern does the same thing for incompatible interfaces.

Real-world Example: Go's io.Reader interface is perfect for adapters. You have a function that expects an io.Reader, but you have a string. Use strings.NewReader() to adapt your string:

1func processFile(r io.Reader) error { /* ... */ }
2
3// Your string becomes an io.Reader
4str := "Hello, World!"
5reader := strings.NewReader(str)
6processFile(reader) // Works!

Key Takeaway: Adapters let you integrate existing code with new interfaces without changing the original code—essential for working with third-party libraries.

Complete Adapter Example:

 1package main
 2
 3import "fmt"
 4
 5// Target interface
 6type MediaPlayer interface {
 7    Play(filename string)
 8}
 9
10// Adaptee - existing incompatible interface
11type AdvancedPlayer struct{}
12
13func (ap *AdvancedPlayer) PlayVLC(filename string) {
14    fmt.Println("Playing VLC file:", filename)
15}
16
17func (ap *AdvancedPlayer) PlayMP4(filename string) {
18    fmt.Println("Playing MP4 file:", filename)
19}
20
21// Adapter - makes AdvancedPlayer compatible with MediaPlayer
22type MediaAdapter struct {
23    advancedPlayer *AdvancedPlayer
24    audioType      string
25}
26
27func NewMediaAdapter(audioType string) *MediaAdapter {
28    return &MediaAdapter{
29        advancedPlayer: &AdvancedPlayer{},
30        audioType:      audioType,
31    }
32}
33
34func (m *MediaAdapter) Play(filename string) {
35    if m.audioType == "vlc" {
36        m.advancedPlayer.PlayVLC(filename)
37    } else if m.audioType == "mp4" {
38        m.advancedPlayer.PlayMP4(filename)
39    }
40}
41
42// Basic player
43type AudioPlayer struct {
44    adapter MediaPlayer
45}
46
47func (ap *AudioPlayer) Play(audioType, filename string) {
48    if audioType == "mp3" {
49        fmt.Println("Playing MP3 file:", filename)
50    } else if audioType == "vlc" || audioType == "mp4" {
51        ap.adapter = NewMediaAdapter(audioType)
52        ap.adapter.Play(filename)
53    } else {
54        fmt.Printf("Invalid media type: %s\n", audioType)
55    }
56}
57
58func main() {
59    player := &AudioPlayer{}
60
61    player.Play("mp3", "song.mp3")
62    player.Play("vlc", "movie.vlc")
63    player.Play("mp4", "video.mp4")
64    player.Play("avi", "file.avi")
65} // run

Bridge Pattern: Decouple Abstraction from Implementation

Problem: Decouple an abstraction from its implementation so both can vary independently.

When to Use:

  • Multiple dimensions of variation (shape × color, device × remote)
  • Avoid cartesian product of subclasses
  • Runtime selection of implementation
  • Platform-independent abstractions
 1package main
 2
 3import "fmt"
 4
 5// Implementation interface
 6type Renderer interface {
 7    RenderCircle(radius float64)
 8    RenderSquare(side float64)
 9}
10
11// Concrete implementations
12type VectorRenderer struct{}
13
14func (vr *VectorRenderer) RenderCircle(radius float64) {
15    fmt.Printf("Drawing circle of radius %.2f (vector)\n", radius)
16}
17
18func (vr *VectorRenderer) RenderSquare(side float64) {
19    fmt.Printf("Drawing square with side %.2f (vector)\n", side)
20}
21
22type RasterRenderer struct{}
23
24func (rr *RasterRenderer) RenderCircle(radius float64) {
25    fmt.Printf("Drawing pixels for circle of radius %.2f\n", radius)
26}
27
28func (rr *RasterRenderer) RenderSquare(side float64) {
29    fmt.Printf("Drawing pixels for square with side %.2f\n", side)
30}
31
32// Abstraction
33type Shape interface {
34    Draw()
35    Resize(factor float64)
36}
37
38// Refined abstractions
39type Circle struct {
40    renderer Renderer
41    radius   float64
42}
43
44func (c *Circle) Draw() {
45    c.renderer.RenderCircle(c.radius)
46}
47
48func (c *Circle) Resize(factor float64) {
49    c.radius *= factor
50}
51
52type Square struct {
53    renderer Renderer
54    side     float64
55}
56
57func (s *Square) Draw() {
58    s.renderer.RenderSquare(s.side)
59}
60
61func (s *Square) Resize(factor float64) {
62    s.side *= factor
63}
64
65func main() {
66    vector := &VectorRenderer{}
67    raster := &RasterRenderer{}
68
69    circle := &Circle{renderer: vector, radius: 5}
70    circle.Draw()
71    circle.Resize(2)
72    circle.Draw()
73
74    square := &Square{renderer: raster, side: 4}
75    square.Draw()
76    square.Resize(0.5)
77    square.Draw()
78} // run

Decorator Pattern: Dynamic Behavior Addition

Problem: Add responsibilities to objects dynamically without modifying their structure.

When to Use:

  • Add functionality to objects without subclassing
  • Combine multiple behaviors flexibly
  • HTTP middleware chains
  • Stream wrappers (compression, encryption)

Imagine: You're ordering coffee. You start with black coffee, then add milk, then sugar, then whipped cream. Each addition "decorates" your coffee without changing the original coffee—you're just adding extra features. The Decorator pattern works the same way.

Real-world Example: HTTP middleware in Go is a perfect use of the Decorator pattern. Each middleware wraps the next handler, adding functionality like logging, authentication, or compression:

1func loggingMiddleware(next http.Handler) http.Handler {
2    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
3        log.Printf("%s %s", r.Method, r.URL.Path)
4        next.ServeHTTP(w, r) // Decorate the original handler
5    })
6}

Important: In Go, prefer function composition over complex decorator hierarchies. If you find yourself creating many nested decorators, consider using middleware chains or functional options instead.

Complete Decorator Example:

 1package main
 2
 3import "fmt"
 4
 5type Coffee interface {
 6    Cost() int
 7    Description() string
 8}
 9
10type SimpleCoffee struct{}
11
12func (SimpleCoffee) Cost() int {
13    return 5
14}
15
16func (SimpleCoffee) Description() string {
17    return "Simple coffee"
18}
19
20// Decorator: Milk
21type MilkDecorator struct {
22    coffee Coffee
23}
24
25func (m MilkDecorator) Cost() int {
26    return m.coffee.Cost() + 2
27}
28
29func (m MilkDecorator) Description() string {
30    return m.coffee.Description() + " + Milk"
31}
32
33// Decorator: Sugar
34type SugarDecorator struct {
35    coffee Coffee
36}
37
38func (s SugarDecorator) Cost() int {
39    return s.coffee.Cost() + 1
40}
41
42func (s SugarDecorator) Description() string {
43    return s.coffee.Description() + " + Sugar"
44}
45
46// Decorator: Whipped Cream
47type WhippedCreamDecorator struct {
48    coffee Coffee
49}
50
51func (w WhippedCreamDecorator) Cost() int {
52    return w.coffee.Cost() + 3
53}
54
55func (w WhippedCreamDecorator) Description() string {
56    return w.coffee.Description() + " + Whipped Cream"
57}
58
59func main() {
60    coffee := SimpleCoffee{}
61    fmt.Printf("%s: $%d\n", coffee.Description(), coffee.Cost())
62
63    coffeeWithMilk := MilkDecorator{coffee: coffee}
64    fmt.Printf("%s: $%d\n", coffeeWithMilk.Description(), coffeeWithMilk.Cost())
65
66    coffeeWithMilkAndSugar := SugarDecorator{coffee: coffeeWithMilk}
67    fmt.Printf("%s: $%d\n", coffeeWithMilkAndSugar.Description(), coffeeWithMilkAndSugar.Cost())
68
69    fancyCoffee := WhippedCreamDecorator{coffee: coffeeWithMilkAndSugar}
70    fmt.Printf("%s: $%d\n", fancyCoffee.Description(), fancyCoffee.Cost())
71} // run

Composite Pattern: Tree Structures

Problem: Compose objects into tree structures to represent part-whole hierarchies, treating individual objects and compositions uniformly.

When to Use:

  • File systems (files and directories)
  • UI component trees
  • Organization hierarchies
  • Menu structures
  • Mathematical expressions

Imagine: You're organizing files on your computer. You have individual files, but you also have folders that can contain other folders and files. Yet you can perform the same operations on both—like "display" or "delete"—and the system handles everything correctly. That's the Composite pattern.

Key Takeaway: The Composite pattern is perfect when you have tree-like structures and want to treat individual objects and groups of objects the same way.

Common Pitfalls:

  1. Overly general design: Don't make the base interface too complex—only include operations that make sense for both leaf and composite objects
  2. Type safety: Be careful with type assertions—use interfaces appropriately to maintain the pattern's benefits
  3. Memory usage: Deep composites can consume significant memory; consider lazy loading for large structures

Real-world Example: File systems, UI component trees, and organization hierarchies all use the Composite pattern. Go's io/fs package uses this pattern—both files and directories implement the same interface.

 1package main
 2
 3import "fmt"
 4
 5type Component interface {
 6    Display(indent int)
 7    Size() int
 8}
 9
10type File struct {
11    name string
12    size int
13}
14
15func (f File) Display(indent int) {
16    for i := 0; i < indent; i++ {
17        fmt.Print("  ")
18    }
19    fmt.Printf("File: %s (%d bytes)\n", f.name, f.size)
20}
21
22func (f File) Size() int {
23    return f.size
24}
25
26type Directory struct {
27    name     string
28    children []Component
29}
30
31func (d *Directory) Add(component Component) {
32    d.children = append(d.children, component)
33}
34
35func (d *Directory) Display(indent int) {
36    for i := 0; i < indent; i++ {
37        fmt.Print("  ")
38    }
39    fmt.Printf("Directory: %s\n", d.name)
40
41    for _, child := range d.children {
42        child.Display(indent + 1)
43    }
44}
45
46func (d *Directory) Size() int {
47    total := 0
48    for _, child := range d.children {
49        total += child.Size()
50    }
51    return total
52}
53
54func main() {
55    root := &Directory{name: "root"}
56    root.Add(File{name: "file1.txt", size: 100})
57    root.Add(File{name: "file2.txt", size: 200})
58
59    subdir := &Directory{name: "subdir"}
60    subdir.Add(File{name: "file3.txt", size: 150})
61    subdir.Add(File{name: "file4.txt", size: 250})
62
63    subsubdir := &Directory{name: "subsubdir"}
64    subsubdir.Add(File{name: "file5.txt", size: 50})
65
66    subdir.Add(subsubdir)
67    root.Add(subdir)
68
69    root.Display(0)
70    fmt.Printf("\nTotal size: %d bytes\n", root.Size())
71} // run

Facade Pattern: Simplified Interface

Problem: Provide a unified, simplified interface to a complex subsystem.

When to Use:

  • Simplify complex libraries or frameworks
  • Create high-level interfaces to lower-level systems
  • Decouple clients from implementation details
  • Layer architecture boundaries
 1package main
 2
 3import "fmt"
 4
 5// Complex subsystem classes
 6type CPU struct{}
 7
 8func (c *CPU) Freeze() {
 9    fmt.Println("CPU: Freezing...")
10}
11
12func (c *CPU) Jump(position int) {
13    fmt.Printf("CPU: Jumping to position %d\n", position)
14}
15
16func (c *CPU) Execute() {
17    fmt.Println("CPU: Executing...")
18}
19
20type Memory struct{}
21
22func (m *Memory) Load(position int, data string) {
23    fmt.Printf("Memory: Loading '%s' at position %d\n", data, position)
24}
25
26type HardDrive struct{}
27
28func (hd *HardDrive) Read(sector int, size int) string {
29    fmt.Printf("HardDrive: Reading %d bytes from sector %d\n", size, sector)
30    return "bootloader_data"
31}
32
33// Facade - simplified interface
34type ComputerFacade struct {
35    cpu       *CPU
36    memory    *Memory
37    hardDrive *HardDrive
38}
39
40func NewComputer() *ComputerFacade {
41    return &ComputerFacade{
42        cpu:       &CPU{},
43        memory:    &Memory{},
44        hardDrive: &HardDrive{},
45    }
46}
47
48func (c *ComputerFacade) Start() {
49    fmt.Println("Starting computer...")
50    c.cpu.Freeze()
51    c.memory.Load(0, c.hardDrive.Read(0, 1024))
52    c.cpu.Jump(0)
53    c.cpu.Execute()
54    fmt.Println("Computer started successfully!")
55}
56
57func main() {
58    computer := NewComputer()
59    computer.Start() // Simple interface hides complexity
60} // run

Proxy Pattern: Controlled Access

Problem: Provide a surrogate or placeholder to control access to an object.

When to Use:

  • Lazy initialization (virtual proxy)
  • Access control (protection proxy)
  • Remote objects (remote proxy)
  • Caching (cache proxy)
  • Logging/monitoring (logging proxy)
 1package main
 2
 3import (
 4    "fmt"
 5    "time"
 6)
 7
 8// Subject interface
 9type Database interface {
10    Query(sql string) string
11}
12
13// Real subject
14type RealDatabase struct{}
15
16func (rd *RealDatabase) Query(sql string) string {
17    time.Sleep(100 * time.Millisecond) // Simulate slow query
18    return fmt.Sprintf("Results for: %s", sql)
19}
20
21// Proxy with caching
22type CachedDatabaseProxy struct {
23    realDB *RealDatabase
24    cache  map[string]string
25}
26
27func NewCachedDatabaseProxy() *CachedDatabaseProxy {
28    return &CachedDatabaseProxy{
29        realDB: &RealDatabase{},
30        cache:  make(map[string]string),
31    }
32}
33
34func (cp *CachedDatabaseProxy) Query(sql string) string {
35    // Check cache first
36    if result, found := cp.cache[sql]; found {
37        fmt.Println("Cache hit!")
38        return result
39    }
40
41    // Cache miss - query real database
42    fmt.Println("Cache miss - querying database...")
43    result := cp.realDB.Query(sql)
44    cp.cache[sql] = result
45    return result
46}
47
48func main() {
49    db := NewCachedDatabaseProxy()
50
51    query := "SELECT * FROM users"
52
53    fmt.Println("First query:")
54    fmt.Println(db.Query(query))
55
56    fmt.Println("\nSecond query (cached):")
57    fmt.Println(db.Query(query))
58} // run

Flyweight Pattern: Shared State Optimization

Problem: Minimize memory usage by sharing common state among multiple objects.

When to Use:

  • Large numbers of similar objects
  • Objects with shared intrinsic state
  • Memory constraints
  • Immutable shared data
 1package main
 2
 3import "fmt"
 4
 5// Flyweight - shared intrinsic state
 6type CharacterStyle struct {
 7    Font   string
 8    Size   int
 9    Color  string
10    IsBold bool
11}
12
13// Flyweight factory
14type StyleFactory struct {
15    styles map[string]*CharacterStyle
16}
17
18func NewStyleFactory() *StyleFactory {
19    return &StyleFactory{
20        styles: make(map[string]*CharacterStyle),
21    }
22}
23
24func (sf *StyleFactory) GetStyle(font string, size int, color string, bold bool) *CharacterStyle {
25    key := fmt.Sprintf("%s-%d-%s-%t", font, size, color, bold)
26
27    if style, exists := sf.styles[key]; exists {
28        return style
29    }
30
31    style := &CharacterStyle{
32        Font:   font,
33        Size:   size,
34        Color:  color,
35        IsBold: bold,
36    }
37    sf.styles[key] = style
38    fmt.Printf("Created new style: %s\n", key)
39
40    return style
41}
42
43// Context - uses flyweight
44type Character struct {
45    char  rune
46    style *CharacterStyle
47}
48
49func main() {
50    factory := NewStyleFactory()
51
52    // Create characters with shared styles
53    chars := []Character{
54        {char: 'H', style: factory.GetStyle("Arial", 12, "black", true)},
55        {char: 'e', style: factory.GetStyle("Arial", 12, "black", false)},
56        {char: 'l', style: factory.GetStyle("Arial", 12, "black", false)},
57        {char: 'l', style: factory.GetStyle("Arial", 12, "black", false)},
58        {char: 'o', style: factory.GetStyle("Arial", 12, "black", false)},
59    }
60
61    // Note: "Arial-12-black-false" is only created once
62    fmt.Printf("\nTotal unique styles created: %d\n", len(factory.styles))
63    fmt.Printf("Total characters: %d\n", len(chars))
64} // run

Behavioral Patterns - Object Interaction

Now let's explore patterns that define how objects interact and communicate with each other. Behavioral patterns focus on responsibilities and the flow of control between objects.

Strategy Pattern: Interchangeable Algorithms

Problem: Define a family of algorithms, encapsulate each one, and make them interchangeable.

When to Use:

  • Multiple algorithms for a specific task
  • Avoid conditional statements for algorithm selection
  • Runtime algorithm switching
  • Isolate algorithm implementation details

Imagine: You're planning a trip from New York to Los Angeles. You could drive, fly, take a train, or bus. Each method gets you there but has different costs, times, and experiences. The Strategy pattern lets you switch between these algorithms easily.

Real-world Example: Sorting algorithms are perfect for the Strategy pattern. Sometimes you need quicksort, sometimes mergesort, sometimes just a simple bubble sort:

 1type SortStrategy interface {
 2    Sort([]int)
 3}
 4
 5// In your application:
 6func sortData(data []int, strategy SortStrategy) {
 7    strategy.Sort(data)
 8}
 9
10// Choose based on data size:
11if len(data) < 100 {
12    sortData(data, &BubbleSort{})
13} else {
14    sortData(data, &QuickSort{})
15}

Important: In Go, the Strategy pattern often simplifies to just using functions as first-class citizens. If your strategy interface has only one method, consider using a function type instead.

Complete Strategy Example:

 1package main
 2
 3import "fmt"
 4
 5type PaymentStrategy interface {
 6    Pay(amount int) string
 7}
 8
 9type CreditCard struct {
10    number string
11}
12
13func (c CreditCard) Pay(amount int) string {
14    return fmt.Sprintf("Paid $%d using Credit Card %s", amount, c.number)
15}
16
17type PayPal struct {
18    email string
19}
20
21func (p PayPal) Pay(amount int) string {
22    return fmt.Sprintf("Paid $%d using PayPal %s", amount, p.email)
23}
24
25type Cash struct{}
26
27func (Cash) Pay(amount int) string {
28    return fmt.Sprintf("Paid $%d in cash", amount)
29}
30
31type Cryptocurrency struct {
32    wallet string
33}
34
35func (c Cryptocurrency) Pay(amount int) string {
36    return fmt.Sprintf("Paid $%d using crypto wallet %s", amount, c.wallet)
37}
38
39type ShoppingCart struct {
40    amount int
41}
42
43func (s *ShoppingCart) Checkout(strategy PaymentStrategy) {
44    result := strategy.Pay(s.amount)
45    fmt.Println(result)
46}
47
48func main() {
49    cart := &ShoppingCart{amount: 100}
50
51    cart.Checkout(CreditCard{number: "1234-5678-9012-3456"})
52    cart.Checkout(PayPal{email: "user@example.com"})
53    cart.Checkout(Cash{})
54    cart.Checkout(Cryptocurrency{wallet: "0x742d35Cc6634C0532925a3b8"})
55} // run

Observer Pattern: Event Notification

Problem: Define a one-to-many dependency so that when one object changes state, all dependents are notified automatically.

When to Use:

  • Event handling systems
  • Model-View relationship
  • Publish-subscribe systems
  • Notification mechanisms

Imagine: You're subscribed to a YouTube channel. When the creator uploads a new video, YouTube automatically notifies all subscribers. You don't have to keep checking—YouTube pushes updates to you. The Observer pattern works exactly like this.

Key Takeaway: Go's channels make the Observer pattern incredibly elegant and efficient. Instead of maintaining lists of observers and calling them one by one, you just send a value to a channel and multiple goroutines can receive it.

When to use Observer vs Channels:

  • Observer pattern: When you have complex state changes and need to notify specific types of observers with different data
  • Channels: When you're just broadcasting events or distributing work

Important: Be careful about goroutine leaks with observers. Always provide a way to unsubscribe and close channels properly. A common mistake is forgetting to handle the case where observers stop listening but the publisher keeps sending.

 1package main
 2
 3import "fmt"
 4
 5type Observer interface {
 6    Update(message string)
 7}
 8
 9type Subject struct {
10    observers []Observer
11}
12
13func (s *Subject) Attach(observer Observer) {
14    s.observers = append(s.observers, observer)
15}
16
17func (s *Subject) Notify(message string) {
18    for _, observer := range s.observers {
19        observer.Update(message)
20    }
21}
22
23type EmailNotifier struct {
24    email string
25}
26
27func (e EmailNotifier) Update(message string) {
28    fmt.Printf("[Email %s] %s\n", e.email, message)
29}
30
31type SMSNotifier struct {
32    phone string
33}
34
35func (s SMSNotifier) Update(message string) {
36    fmt.Printf("[SMS %s] %s\n", s.phone, message)
37}
38
39type PushNotifier struct {
40    deviceID string
41}
42
43func (p PushNotifier) Update(message string) {
44    fmt.Printf("[Push %s] %s\n", p.deviceID, message)
45}
46
47func main() {
48    subject := &Subject{}
49
50    subject.Attach(EmailNotifier{email: "user@example.com"})
51    subject.Attach(SMSNotifier{phone: "+1234567890"})
52    subject.Attach(PushNotifier{deviceID: "device-123"})
53
54    subject.Notify("Your order has been shipped!")
55    fmt.Println()
56    subject.Notify("Your package will arrive today!")
57} // run

Command Pattern: Encapsulated Requests

Problem: Encapsulate a request as an object, allowing parameterization and queuing of requests.

When to Use:

  • Undo/redo functionality
  • Task queuing
  • Transaction logging
  • Macro recording
  • Deferred execution

Imagine: You're using a remote control for your TV. Each button represents a command, but the remote doesn't need to know how each command works—it just sends the signal. The TV handles the actual work. The Command pattern turns requests into standalone objects.

Real-world Example: Job queues are a perfect use of the Command pattern. Each job is a command that can be queued, executed, retried, and undone:

 1type Job interface {
 2    Execute() error
 3    Undo() error
 4}
 5
 6// Queue jobs for later execution
 7queue := []Job{
 8    &SendEmailJob{to: "user@example.com"},
 9    &ProcessPaymentJob{amount: 99.99},
10    &UpdateInventoryJob{product: "abc123"},
11}
12
13// Execute them in order
14for _, job := range queue {
15    job.Execute()
16}

Common Pitfalls:

  1. Over-engineering: Don't use Command pattern for simple method calls. It's most valuable when you need to queue, undo, or parameterize commands.
  2. Memory leaks: Be careful with command queues that grow indefinitely. Implement size limits and proper cleanup.
  3. Error handling: Each command should handle its own errors gracefully and communicate failures properly.
 1package main
 2
 3import "fmt"
 4
 5type Command interface {
 6    Execute()
 7    Undo()
 8}
 9
10type Light struct {
11    isOn bool
12}
13
14func (l *Light) TurnOn() {
15    l.isOn = true
16    fmt.Println("Light is ON")
17}
18
19func (l *Light) TurnOff() {
20    l.isOn = false
21    fmt.Println("Light is OFF")
22}
23
24type LightOnCommand struct {
25    light *Light
26}
27
28func (c *LightOnCommand) Execute() {
29    c.light.TurnOn()
30}
31
32func (c *LightOnCommand) Undo() {
33    c.light.TurnOff()
34}
35
36type LightOffCommand struct {
37    light *Light
38}
39
40func (c *LightOffCommand) Execute() {
41    c.light.TurnOff()
42}
43
44func (c *LightOffCommand) Undo() {
45    c.light.TurnOn()
46}
47
48type RemoteControl struct {
49    commands []Command
50    history  []Command
51}
52
53func (r *RemoteControl) Submit(command Command) {
54    command.Execute()
55    r.history = append(r.history, command)
56}
57
58func (r *RemoteControl) Undo() {
59    if len(r.history) == 0 {
60        return
61    }
62
63    lastCommand := r.history[len(r.history)-1]
64    lastCommand.Undo()
65    r.history = r.history[:len(r.history)-1]
66}
67
68func main() {
69    light := &Light{}
70    remote := &RemoteControl{}
71
72    turnOn := &LightOnCommand{light: light}
73    turnOff := &LightOffCommand{light: light}
74
75    remote.Submit(turnOn)
76    remote.Submit(turnOff)
77    remote.Submit(turnOn)
78
79    fmt.Println("\nUndoing last command:")
80    remote.Undo()
81} // run

Iterator Pattern: Sequential Access

Problem: Provide a way to access elements of an aggregate object sequentially without exposing its underlying representation.

When to Use:

  • Custom collection traversal
  • Multiple simultaneous iterations
  • Different traversal algorithms
  • Hiding collection implementation details

Note: Go's range keyword handles most iteration needs, but custom iterators are useful for complex data structures.

 1package main
 2
 3import "fmt"
 4
 5// Iterator interface
 6type Iterator interface {
 7    HasNext() bool
 8    Next() interface{}
 9}
10
11// Collection interface
12type Collection interface {
13    CreateIterator() Iterator
14}
15
16// Concrete collection
17type BookCollection struct {
18    books []string
19}
20
21func (bc *BookCollection) CreateIterator() Iterator {
22    return &BookIterator{
23        books: bc.books,
24        index: 0,
25    }
26}
27
28// Concrete iterator
29type BookIterator struct {
30    books []string
31    index int
32}
33
34func (bi *BookIterator) HasNext() bool {
35    return bi.index < len(bi.books)
36}
37
38func (bi *BookIterator) Next() interface{} {
39    if bi.HasNext() {
40        book := bi.books[bi.index]
41        bi.index++
42        return book
43    }
44    return nil
45}
46
47func main() {
48    collection := &BookCollection{
49        books: []string{
50            "Design Patterns",
51            "Clean Code",
52            "The Pragmatic Programmer",
53            "Refactoring",
54        },
55    }
56
57    iterator := collection.CreateIterator()
58
59    fmt.Println("Books in collection:")
60    for iterator.HasNext() {
61        book := iterator.Next().(string)
62        fmt.Printf("- %s\n", book)
63    }
64} // run

Template Method Pattern: Algorithm Skeleton

Problem: Define the skeleton of an algorithm in a base class, letting subclasses override specific steps.

When to Use:

  • Common algorithm structure with varying steps
  • Code reuse for similar algorithms
  • Framework extension points
  • Invariant parts vs customizable parts

Go Approach: Use higher-order functions instead of inheritance.

 1package main
 2
 3import "fmt"
 4
 5// Template function
 6func ProcessDocument(filename string, parse func(string) string, analyze func(string) string, save func(string)) {
 7    // Step 1: Read (invariant)
 8    fmt.Printf("Reading %s...\n", filename)
 9    content := fmt.Sprintf("content of %s", filename)
10
11    // Step 2: Parse (customizable)
12    parsed := parse(content)
13
14    // Step 3: Analyze (customizable)
15    analyzed := analyze(parsed)
16
17    // Step 4: Save (customizable)
18    save(analyzed)
19}
20
21func main() {
22    // Process CSV
23    ProcessDocument(
24        "data.csv",
25        func(content string) string {
26            fmt.Println("Parsing CSV...")
27            return "parsed CSV: " + content
28        },
29        func(parsed string) string {
30            fmt.Println("Analyzing CSV...")
31            return "analyzed: " + parsed
32        },
33        func(result string) {
34            fmt.Printf("Saving CSV result: %s\n\n", result)
35        },
36    )
37
38    // Process JSON
39    ProcessDocument(
40        "data.json",
41        func(content string) string {
42            fmt.Println("Parsing JSON...")
43            return "parsed JSON: " + content
44        },
45        func(parsed string) string {
46            fmt.Println("Analyzing JSON...")
47            return "analyzed: " + parsed
48        },
49        func(result string) {
50            fmt.Printf("Saving JSON result: %s\n", result)
51        },
52    )
53} // run

State Pattern: Object Behavior Based on State

Problem: Allow an object to alter its behavior when its internal state changes.

When to Use:

  • Objects with state-dependent behavior
  • Large conditional statements based on state
  • State transitions need to be explicit
  • State-specific behavior needs encapsulation
  1package main
  2
  3import "fmt"
  4
  5// State interface
  6type State interface {
  7    Handle(context *Context) string
  8}
  9
 10// Context
 11type Context struct {
 12    state State
 13}
 14
 15func (c *Context) SetState(state State) {
 16    c.state = state
 17}
 18
 19func (c *Context) Request() string {
 20    return c.state.Handle(c)
 21}
 22
 23// Concrete states
 24type ConcreteStateA struct{}
 25
 26func (s *ConcreteStateA) Handle(context *Context) string {
 27    context.SetState(&ConcreteStateB{})
 28    return "State A handled. Transitioning to State B."
 29}
 30
 31type ConcreteStateB struct{}
 32
 33func (s *ConcreteStateB) Handle(context *Context) string {
 34    context.SetState(&ConcreteStateC{})
 35    return "State B handled. Transitioning to State C."
 36}
 37
 38type ConcreteStateC struct{}
 39
 40func (s *ConcreteStateC) Handle(context *Context) string {
 41    context.SetState(&ConcreteStateA{})
 42    return "State C handled. Transitioning to State A."
 43}
 44
 45// Real-world example: TCP connection
 46type TCPState interface {
 47    Open() string
 48    Close() string
 49    Acknowledge() string
 50}
 51
 52type TCPConnection struct {
 53    state TCPState
 54}
 55
 56func (tc *TCPConnection) Open() string {
 57    return tc.state.Open()
 58}
 59
 60func (tc *TCPConnection) Close() string {
 61    return tc.state.Close()
 62}
 63
 64type TCPClosed struct {
 65    connection *TCPConnection
 66}
 67
 68func (t *TCPClosed) Open() string {
 69    t.connection.state = &TCPOpen{connection: t.connection}
 70    return "Connection opened"
 71}
 72
 73func (t *TCPClosed) Close() string {
 74    return "Already closed"
 75}
 76
 77func (t *TCPClosed) Acknowledge() string {
 78    return "Cannot acknowledge - connection closed"
 79}
 80
 81type TCPOpen struct {
 82    connection *TCPConnection
 83}
 84
 85func (t *TCPOpen) Open() string {
 86    return "Already open"
 87}
 88
 89func (t *TCPOpen) Close() string {
 90    t.connection.state = &TCPClosed{connection: t.connection}
 91    return "Connection closed"
 92}
 93
 94func (t *TCPOpen) Acknowledge() string {
 95    return "Acknowledged"
 96}
 97
 98func main() {
 99    context := &Context{state: &ConcreteStateA{}}
100
101    fmt.Println(context.Request())
102    fmt.Println(context.Request())
103    fmt.Println(context.Request())
104    fmt.Println(context.Request()) // Back to A
105} // run

Chain of Responsibility: Request Handling Chain

Problem: Pass requests along a chain of handlers until one handles it.

When to Use:

  • Multiple objects can handle a request
  • Handler is determined at runtime
  • Request should be handled by one of several objects
  • Middleware pipelines
 1package main
 2
 3import "fmt"
 4
 5// Handler interface
 6type Handler interface {
 7    SetNext(handler Handler)
 8    Handle(request string) string
 9}
10
11// Base handler
12type BaseHandler struct {
13    next Handler
14}
15
16func (h *BaseHandler) SetNext(handler Handler) {
17    h.next = handler
18}
19
20// Concrete handlers
21type AuthenticationHandler struct {
22    BaseHandler
23}
24
25func (h *AuthenticationHandler) Handle(request string) string {
26    if request == "authenticated" {
27        fmt.Println("AuthenticationHandler: Authenticated")
28        if h.next != nil {
29            return h.next.Handle(request)
30        }
31        return "Success"
32    }
33    return "AuthenticationHandler: Authentication failed"
34}
35
36type AuthorizationHandler struct {
37    BaseHandler
38}
39
40func (h *AuthorizationHandler) Handle(request string) string {
41    fmt.Println("AuthorizationHandler: Checking permissions")
42    if h.next != nil {
43        return h.next.Handle(request)
44    }
45    return "Success"
46}
47
48type ValidationHandler struct {
49    BaseHandler
50}
51
52func (h *ValidationHandler) Handle(request string) string {
53    fmt.Println("ValidationHandler: Validating data")
54    if h.next != nil {
55        return h.next.Handle(request)
56    }
57    return "Success"
58}
59
60func main() {
61    // Build chain: Auth -> Authz -> Validation
62    auth := &AuthenticationHandler{}
63    authz := &AuthorizationHandler{}
64    valid := &ValidationHandler{}
65
66    auth.SetNext(authz)
67    authz.SetNext(valid)
68
69    // Process request
70    fmt.Println("\nProcessing authenticated request:")
71    result := auth.Handle("authenticated")
72    fmt.Printf("Result: %s\n", result)
73
74    fmt.Println("\nProcessing unauthenticated request:")
75    result = auth.Handle("unauthenticated")
76    fmt.Printf("Result: %s\n", result)
77} // run

Mediator Pattern: Centralized Communication

Problem: Define an object that encapsulates how a set of objects interact, promoting loose coupling.

When to Use:

  • Complex communication between many objects
  • Objects should be reusable without their interactions
  • Centralized control of interactions
  • Reduce direct object dependencies
 1package main
 2
 3import "fmt"
 4
 5// Mediator interface
 6type ChatMediator interface {
 7    SendMessage(message string, user User)
 8    AddUser(user User)
 9}
10
11// Concrete mediator
12type ChatRoom struct {
13    users []User
14}
15
16func (cr *ChatRoom) SendMessage(message string, sender User) {
17    for _, user := range cr.users {
18        if user.GetName() != sender.GetName() {
19            user.Receive(message, sender.GetName())
20        }
21    }
22}
23
24func (cr *ChatRoom) AddUser(user User) {
25    cr.users = append(cr.users, user)
26}
27
28// Colleague interface
29type User interface {
30    Send(message string)
31    Receive(message string, from string)
32    GetName() string
33}
34
35// Concrete colleague
36type ChatUser struct {
37    name     string
38    mediator ChatMediator
39}
40
41func (cu *ChatUser) Send(message string) {
42    fmt.Printf("%s sends: %s\n", cu.name, message)
43    cu.mediator.SendMessage(message, cu)
44}
45
46func (cu *ChatUser) Receive(message string, from string) {
47    fmt.Printf("%s received from %s: %s\n", cu.name, from, message)
48}
49
50func (cu *ChatUser) GetName() string {
51    return cu.name
52}
53
54func main() {
55    mediator := &ChatRoom{}
56
57    alice := &ChatUser{name: "Alice", mediator: mediator}
58    bob := &ChatUser{name: "Bob", mediator: mediator}
59    charlie := &ChatUser{name: "Charlie", mediator: mediator}
60
61    mediator.AddUser(alice)
62    mediator.AddUser(bob)
63    mediator.AddUser(charlie)
64
65    alice.Send("Hello everyone!")
66    fmt.Println()
67    bob.Send("Hi Alice!")
68} // run

Memento Pattern: State Snapshot

Problem: Capture and restore an object's internal state without violating encapsulation.

When to Use:

  • Undo/redo mechanisms
  • Snapshot functionality
  • Transaction rollback
  • State history
 1package main
 2
 3import "fmt"
 4
 5// Memento
 6type Memento struct {
 7    state string
 8}
 9
10// Originator
11type TextEditor struct {
12    content string
13}
14
15func (te *TextEditor) Write(text string) {
16    te.content += text
17}
18
19func (te *TextEditor) Save() *Memento {
20    return &Memento{state: te.content}
21}
22
23func (te *TextEditor) Restore(m *Memento) {
24    te.content = m.state
25}
26
27func (te *TextEditor) GetContent() string {
28    return te.content
29}
30
31// Caretaker
32type History struct {
33    mementos []*Memento
34}
35
36func (h *History) Push(m *Memento) {
37    h.mementos = append(h.mementos, m)
38}
39
40func (h *History) Pop() *Memento {
41    if len(h.mementos) == 0 {
42        return nil
43    }
44
45    last := h.mementos[len(h.mementos)-1]
46    h.mementos = h.mementos[:len(h.mementos)-1]
47    return last
48}
49
50func main() {
51    editor := &TextEditor{}
52    history := &History{}
53
54    editor.Write("First sentence. ")
55    history.Push(editor.Save())
56
57    editor.Write("Second sentence. ")
58    history.Push(editor.Save())
59
60    editor.Write("Third sentence. ")
61    fmt.Printf("Current: %s\n", editor.GetContent())
62
63    // Undo
64    editor.Restore(history.Pop())
65    fmt.Printf("After undo: %s\n", editor.GetContent())
66
67    editor.Restore(history.Pop())
68    fmt.Printf("After undo: %s\n", editor.GetContent())
69} // run

Visitor Pattern: Operations on Object Structures

Problem: Define a new operation without changing the classes of the elements on which it operates.

When to Use:

  • Many distinct operations on an object structure
  • Object structure rarely changes but operations change often
  • Algorithms depend on concrete classes
  • Type-specific behavior across a hierarchy

Go Approach: Use type switches instead of double dispatch.

  1package main
  2
  3import "fmt"
  4
  5// Element interface
  6type Shape interface {
  7    Accept(visitor ShapeVisitor)
  8}
  9
 10// Visitor interface
 11type ShapeVisitor interface {
 12    VisitCircle(circle *Circle)
 13    VisitRectangle(rectangle *Rectangle)
 14    VisitTriangle(triangle *Triangle)
 15}
 16
 17// Concrete elements
 18type Circle struct {
 19    Radius float64
 20}
 21
 22func (c *Circle) Accept(visitor ShapeVisitor) {
 23    visitor.VisitCircle(c)
 24}
 25
 26type Rectangle struct {
 27    Width, Height float64
 28}
 29
 30func (r *Rectangle) Accept(visitor ShapeVisitor) {
 31    visitor.VisitRectangle(r)
 32}
 33
 34type Triangle struct {
 35    Base, Height float64
 36}
 37
 38func (t *Triangle) Accept(visitor ShapeVisitor) {
 39    visitor.VisitTriangle(t)
 40}
 41
 42// Concrete visitors
 43type AreaCalculator struct {
 44    totalArea float64
 45}
 46
 47func (ac *AreaCalculator) VisitCircle(circle *Circle) {
 48    area := 3.14159 * circle.Radius * circle.Radius
 49    fmt.Printf("Circle area: %.2f\n", area)
 50    ac.totalArea += area
 51}
 52
 53func (ac *AreaCalculator) VisitRectangle(rectangle *Rectangle) {
 54    area := rectangle.Width * rectangle.Height
 55    fmt.Printf("Rectangle area: %.2f\n", area)
 56    ac.totalArea += area
 57}
 58
 59func (ac *AreaCalculator) VisitTriangle(triangle *Triangle) {
 60    area := (triangle.Base * triangle.Height) / 2
 61    fmt.Printf("Triangle area: %.2f\n", area)
 62    ac.totalArea += area
 63}
 64
 65func (ac *AreaCalculator) GetTotalArea() float64 {
 66    return ac.totalArea
 67}
 68
 69type PerimeterCalculator struct{}
 70
 71func (pc *PerimeterCalculator) VisitCircle(circle *Circle) {
 72    perimeter := 2 * 3.14159 * circle.Radius
 73    fmt.Printf("Circle perimeter: %.2f\n", perimeter)
 74}
 75
 76func (pc *PerimeterCalculator) VisitRectangle(rectangle *Rectangle) {
 77    perimeter := 2 * (rectangle.Width + rectangle.Height)
 78    fmt.Printf("Rectangle perimeter: %.2f\n", perimeter)
 79}
 80
 81func (pc *PerimeterCalculator) VisitTriangle(triangle *Triangle) {
 82    // Simplified - assumes equilateral for demo
 83    perimeter := triangle.Base * 3
 84    fmt.Printf("Triangle perimeter (approx): %.2f\n", perimeter)
 85}
 86
 87func main() {
 88    shapes := []Shape{
 89        &Circle{Radius: 5},
 90        &Rectangle{Width: 4, Height: 6},
 91        &Triangle{Base: 3, Height: 4},
 92    }
 93
 94    fmt.Println("Calculating areas:")
 95    areaCalc := &AreaCalculator{}
 96    for _, shape := range shapes {
 97        shape.Accept(areaCalc)
 98    }
 99    fmt.Printf("Total area: %.2f\n\n", areaCalc.GetTotalArea())
100
101    fmt.Println("Calculating perimeters:")
102    perimCalc := &PerimeterCalculator{}
103    for _, shape := range shapes {
104        shape.Accept(perimCalc)
105    }
106} // run

Concurrency Patterns - Go's Strength

Go's approach to concurrency isn't just an afterthought—it's a core feature that creates entirely new patterns not possible or practical in other languages. Goroutines and channels enable elegant solutions to concurrent problems.

Worker Pool: Controlled Concurrency

Problem: Process many tasks efficiently without overwhelming system resources.

When to Use:

  • CPU-bound tasks that benefit from parallelism
  • Rate limiting concurrent operations
  • Bounded resource usage
  • Backpressure handling

Imagine: You're running a restaurant kitchen during dinner rush. You have 20 orders to prepare but only 3 chefs. Instead of hiring 20 chefs or making one chef handle all orders, you have 3 chefs working continuously, picking up orders as they come in. That's exactly how a worker pool works.

Key Takeaway: Worker pools limit resource usage while maximizing throughput. They're perfect for CPU-bound tasks where you want to utilize all available CPU cores without overwhelming the system.

Real-world Example: Image processing services use worker pools extensively. Instead of processing 1000 images sequentially or spawning 1000 goroutines, they use a pool of workers equal to the number of CPU cores:

1// Process images in parallel with N workers
2workers := runtime.NumCPU()

Important: Always close channels when you're done sending work. A common mistake is forgetting to close the jobs channel, which causes workers to wait forever.

 1package main
 2
 3import (
 4    "fmt"
 5    "sync"
 6    "time"
 7)
 8
 9func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
10    defer wg.Done()
11
12    for job := range jobs {
13        fmt.Printf("Worker %d processing job %d\n", id, job)
14        time.Sleep(time.Second) // Simulate work
15        results <- job * 2
16    }
17}
18
19func main() {
20    jobs := make(chan int, 10)
21    results := make(chan int, 10)
22
23    var wg sync.WaitGroup
24
25    // Start 3 workers
26    for w := 1; w <= 3; w++ {
27        wg.Add(1)
28        go worker(w, jobs, results, &wg)
29    }
30
31    // Send 9 jobs
32    for j := 1; j <= 9; j++ {
33        jobs <- j
34    }
35    close(jobs)
36
37    // Close results when all workers done
38    go func() {
39        wg.Wait()
40        close(results)
41    }()
42
43    // Collect results
44    for result := range results {
45        fmt.Println("Result:", result)
46    }
47} // run

Pipeline Pattern: Chained Processing

Problem: Chain multiple processing stages where output of one stage becomes input of the next.

When to Use:

  • Multi-stage data transformation
  • Streaming data processing
  • Producer-consumer chains
  • ETL pipelines
 1package main
 2
 3import "fmt"
 4
 5func generator(nums ...int) <-chan int {
 6    out := make(chan int)
 7    go func() {
 8        for _, n := range nums {
 9            out <- n
10        }
11        close(out)
12    }()
13    return out
14}
15
16func square(in <-chan int) <-chan int {
17    out := make(chan int)
18    go func() {
19        for n := range in {
20            out <- n * n
21        }
22        close(out)
23    }()
24    return out
25}
26
27func filter(in <-chan int, predicate func(int) bool) <-chan int {
28    out := make(chan int)
29    go func() {
30        for n := range in {
31            if predicate(n) {
32                out <- n
33            }
34        }
35        close(out)
36    }()
37    return out
38}
39
40func main() {
41    // Create pipeline
42    numbers := generator(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
43    squared := square(numbers)
44    evens := filter(squared, func(n int) bool {
45        return n%2 == 0
46    })
47
48    // Consume results
49    for n := range evens {
50        fmt.Println(n)
51    }
52} // run

Fan-Out/Fan-In: Parallel Processing and Merging

Problem: Distribute work across multiple goroutines and collect results.

When to Use:

  • Parallelize independent tasks
  • Aggregate results from multiple sources
  • Load balancing
  • Scatter-gather pattern
 1package main
 2
 3import (
 4    "fmt"
 5    "sync"
 6)
 7
 8func producer(nums ...int) <-chan int {
 9    out := make(chan int)
10    go func() {
11        for _, n := range nums {
12            out <- n
13        }
14        close(out)
15    }()
16    return out
17}
18
19func square(in <-chan int) <-chan int {
20    out := make(chan int)
21    go func() {
22        for n := range in {
23            out <- n * n
24        }
25        close(out)
26    }()
27    return out
28}
29
30func merge(channels ...<-chan int) <-chan int {
31    var wg sync.WaitGroup
32    out := make(chan int)
33
34    output := func(c <-chan int) {
35        defer wg.Done()
36        for n := range c {
37            out <- n
38        }
39    }
40
41    wg.Add(len(channels))
42    for _, c := range channels {
43        go output(c)
44    }
45
46    go func() {
47        wg.Wait()
48        close(out)
49    }()
50
51    return out
52}
53
54func main() {
55    in := producer(1, 2, 3, 4, 5, 6, 7, 8)
56
57    // Fan-out: distribute work to 3 workers
58    c1 := square(in)
59    c2 := square(in)
60    c3 := square(in)
61
62    // Fan-in: merge results
63    for n := range merge(c1, c2, c3) {
64        fmt.Println(n)
65    }
66} // run

Select Pattern: Multiplexing Channels

Problem: Handle multiple channel operations simultaneously.

When to Use:

  • Timeout operations
  • Non-blocking channel operations
  • Multiple channel monitoring
  • Cancellation and context handling
 1package main
 2
 3import (
 4    "fmt"
 5    "time"
 6)
 7
 8func main() {
 9    c1 := make(chan string)
10    c2 := make(chan string)
11
12    go func() {
13        time.Sleep(1 * time.Second)
14        c1 <- "result from c1"
15    }()
16
17    go func() {
18        time.Sleep(2 * time.Second)
19        c2 <- "result from c2"
20    }()
21
22    // Wait for both with timeout
23    timeout := time.After(3 * time.Second)
24
25    for i := 0; i < 2; i++ {
26        select {
27        case msg1 := <-c1:
28            fmt.Println("Received:", msg1)
29        case msg2 := <-c2:
30            fmt.Println("Received:", msg2)
31        case <-timeout:
32            fmt.Println("Timeout!")
33            return
34        }
35    }
36} // run

Context Pattern: Cancellation and Deadlines

Problem: Manage cancellation, deadlines, and request-scoped values across API boundaries.

When to Use:

  • Request cancellation propagation
  • Timeout management
  • Request-scoped values
  • Graceful shutdown
 1package main
 2
 3import (
 4    "context"
 5    "fmt"
 6    "time"
 7)
 8
 9func operation(ctx context.Context, duration time.Duration) {
10    select {
11    case <-time.After(duration):
12        fmt.Println("Operation completed")
13    case <-ctx.Done():
14        fmt.Println("Operation cancelled:", ctx.Err())
15    }
16}
17
18func main() {
19    // With timeout
20    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
21    defer cancel()
22
23    fmt.Println("Starting operation with 2s timeout...")
24    operation(ctx, 1*time.Second) // Completes
25
26    fmt.Println("\nStarting operation with 2s timeout...")
27    operation(ctx, 3*time.Second) // Times out
28
29    // With cancellation
30    ctx2, cancel2 := context.WithCancel(context.Background())
31
32    go func() {
33        time.Sleep(500 * time.Millisecond)
34        cancel2()
35    }()
36
37    fmt.Println("\nStarting operation with manual cancellation...")
38    operation(ctx2, 2*time.Second) // Cancelled
39} // run

Go-Specific Patterns - Idiomatic Go

Interface Segregation: Small, Focused Interfaces

Problem: Clients shouldn't be forced to depend on methods they don't use.

When to Use:

  • Building flexible, composable interfaces
  • Avoiding bloated interfaces
  • Supporting multiple interface roles
  • Interface-based design
 1package main
 2
 3import "fmt"
 4
 5// Small, focused interfaces
 6type Reader interface {
 7    Read() string
 8}
 9
10type Writer interface {
11    Write(data string)
12}
13
14type ReadWriter interface {
15    Reader
16    Writer
17}
18
19type Closer interface {
20    Close() error
21}
22
23type ReadWriteCloser interface {
24    Reader
25    Writer
26    Closer
27}
28
29type File struct {
30    data   string
31    closed bool
32}
33
34func (f *File) Read() string {
35    return f.data
36}
37
38func (f *File) Write(data string) {
39    f.data = data
40}
41
42func (f *File) Close() error {
43    f.closed = true
44    fmt.Println("File closed")
45    return nil
46}
47
48func main() {
49    file := &File{}
50
51    // Can be used as Writer
52    var w Writer = file
53    w.Write("Hello, World!")
54
55    // Can be used as Reader
56    var r Reader = file
57    fmt.Println(r.Read())
58
59    // Can be used as ReadWriter
60    var rw ReadWriter = file
61    fmt.Println(rw.Read())
62
63    // Can be used as ReadWriteCloser
64    var rwc ReadWriteCloser = file
65    rwc.Close()
66} // run

Embed for Composition: Favor Composition Over Inheritance

Problem: Reuse behavior without inheritance hierarchies.

When to Use:

  • Code reuse without inheritance
  • Building complex types from simple ones
  • Method delegation
  • Extending types
 1package main
 2
 3import "fmt"
 4
 5type Engine struct {
 6    horsepower int
 7}
 8
 9func (e *Engine) Start() {
10    fmt.Println("Engine started")
11}
12
13func (e *Engine) Stop() {
14    fmt.Println("Engine stopped")
15}
16
17type GPS struct{}
18
19func (g *GPS) Navigate(destination string) {
20    fmt.Printf("Navigating to %s\n", destination)
21}
22
23// Composition through embedding
24type Car struct {
25    Engine // Embedded
26    GPS    // Embedded
27    brand  string
28}
29
30func (c *Car) Drive() {
31    fmt.Printf("Driving %s\n", c.brand)
32}
33
34func main() {
35    car := Car{
36        Engine: Engine{horsepower: 200},
37        GPS:    GPS{},
38        brand:  "Toyota",
39    }
40
41    // Can call Engine methods directly
42    car.Start()
43    car.Drive()
44    car.Navigate("San Francisco")
45    car.Stop()
46
47    fmt.Printf("Horsepower: %d\n", car.horsepower)
48} // run

Functional Options with Error Handling

Problem: Configure objects with validation and error propagation.

When to Use:

  • Configuration with validation
  • Error handling during construction
  • Complex initialization logic
  • Builder pattern with errors
 1package main
 2
 3import (
 4    "errors"
 5    "fmt"
 6)
 7
 8type Config struct {
 9    host    string
10    port    int
11    timeout int
12}
13
14type Option func(*Config) error
15
16func WithHost(host string) Option {
17    return func(c *Config) error {
18        if host == "" {
19            return errors.New("host cannot be empty")
20        }
21        c.host = host
22        return nil
23    }
24}
25
26func WithPort(port int) Option {
27    return func(c *Config) error {
28        if port < 1 || port > 65535 {
29            return fmt.Errorf("invalid port: %d", port)
30        }
31        c.port = port
32        return nil
33    }
34}
35
36func WithTimeout(timeout int) Option {
37    return func(c *Config) error {
38        if timeout < 0 {
39            return errors.New("timeout cannot be negative")
40        }
41        c.timeout = timeout
42        return nil
43    }
44}
45
46func NewConfig(opts ...Option) (*Config, error) {
47    config := &Config{
48        host:    "localhost",
49        port:    8080,
50        timeout: 30,
51    }
52
53    for _, opt := range opts {
54        if err := opt(config); err != nil {
55            return nil, err
56        }
57    }
58
59    return config, nil
60}
61
62func main() {
63    // Valid configuration
64    config, err := NewConfig(
65        WithHost("example.com"),
66        WithPort(443),
67        WithTimeout(60),
68    )
69    if err != nil {
70        fmt.Printf("Error: %v\n", err)
71        return
72    }
73    fmt.Printf("Valid config: %+v\n", config)
74
75    // Invalid configuration
76    _, err = NewConfig(
77        WithPort(99999), // Invalid
78    )
79    fmt.Printf("Expected error: %v\n", err)
80} // run

Advanced Architectural Patterns

CQRS: Command Query Responsibility Segregation

Problem: Separate read and write operations for better scalability and performance.

When to Use:

  • Different read/write optimization requirements
  • Complex domains with different data models
  • Event sourcing architectures
  • Microservices with separate read/write services

Separate read and write operations for better scalability and performance. This pattern is extensively covered in the article with a complete implementation example including command stores, query stores, and event buses.

Event Sourcing Pattern

Problem: Store all changes as a sequence of events rather than current state.

When to Use:

  • Audit trail requirements
  • Time-travel debugging
  • Event replay capability
  • CQRS implementation

Store state changes as a sequence of events. The article provides a full implementation with event store, aggregate rebuilding, and command handlers.

Saga Pattern: Distributed Transactions

Problem: Manage distributed transactions with compensating actions.

When to Use:

  • Microservices transactions
  • Long-running business processes
  • Distributed system coordination
  • Failure recovery scenarios

Manage distributed transactions with compensating actions. The article includes a complete saga orchestrator implementation with step execution and compensation logic.

Best Practices and Anti-Patterns

When to Use Patterns

Use patterns when:

  1. The problem matches the pattern's intent
  2. Benefits outweigh complexity
  3. Team understands the pattern
  4. Future changes are anticipated

Don't use patterns when:

  1. Simple solution exists
  2. Problem doesn't match pattern
  3. Over-engineering for current needs
  4. Pattern adds unnecessary complexity

Common Anti-Patterns to Avoid

1. Pattern Overload

  • Don't force patterns where they don't fit
  • Start simple, add patterns when needed
  • Refactor to patterns, don't start with them

2. God Object

  • Objects doing too much
  • Violates Single Responsibility Principle
  • Hard to test and maintain

3. Premature Optimization

  • Don't optimize before profiling
  • Patterns for flexibility, not performance
  • Measure before optimizing

4. Copy-Paste Inheritance

  • Duplicating code instead of composing
  • Should use composition or interfaces
  • Leads to maintenance nightmares

5. Golden Hammer

  • Using same pattern for everything
  • "If all you have is a hammer, everything looks like a nail"
  • Choose right tool for the job

Best Practices Summary

  1. Favor composition over inheritance - Use embedding
  2. Keep interfaces small - Easier to implement and test
  3. Accept interfaces, return structs - More flexible APIs
  4. Use functional options for complex constructors - Clean and extensible
  5. Leverage goroutines and channels - For concurrent patterns
  6. Don't overuse patterns - Keep code simple and readable
  7. Prefer standard library patterns - io.Reader, io.Writer, etc.
  8. CQRS for complex domains - Separate reads and writes when scalability matters
  9. Event sourcing for audit trails - Store all state changes as events
  10. Saga for distributed transactions - Coordinate multiple services with compensation

Pattern Selection Guide

Problem Go Solution Avoid
Need optional parameters Functional options Multiple constructors
Want to extend behavior Middleware functions Complex decorators
Multiple implementations Interface + functions Inheritance hierarchies
Complex construction Builder pattern Giant constructors
Need notifications Channels Manual observer lists
Algorithm selection Function types Strategy classes
State-dependent behavior State pattern Large switch statements
Undo/redo Command pattern Global state
Tree structures Composite pattern Type assertions everywhere

Common Pitfalls and How to Avoid Them

When Patterns Go Wrong

Even with the best patterns, it's easy to make mistakes. Here are the most common pitfalls and how to avoid them:

1. Over-engineering - The biggest trap. Don't use a pattern when a simple function works.

  • Bad: Creating a Strategy pattern for a single algorithm
  • Good: Just use the algorithm directly

2. Wrong pattern choice - Understand the problem first, then pick the pattern.

  • Bad: Using Builder when Functional Options would be simpler
  • Good: Match the pattern to the complexity

3. Ignoring Go idioms - Don't fight the language.

  • Bad: Implementing Observer pattern manually when channels work better
  • Good: Use Go's strengths

4. Too many abstractions - Each layer adds complexity.

  • Bad: AbstractFactory → ConcreteFactory → AbstractProduct → ConcreteProduct
  • Good: Simple factory function returning an interface

5. Premature optimization - Patterns should solve real problems.

  • Bad: "We might need caching later, let's implement a complex pattern"
  • Good: Add patterns when you actually need them

6. Not using interfaces - Interfaces enable most Go patterns.

  • Bad: Working with concrete types everywhere
  • Good: Program to interfaces, not implementations

7. Forgetting error handling - Patterns must handle errors gracefully.

  • Bad: Assuming all operations succeed
  • Good: Propagate errors through pattern implementation

8. Goroutine leaks - Concurrent patterns must clean up resources.

  • Bad: Starting goroutines without cleanup mechanism
  • Good: Use context cancellation and WaitGroups

Practice Exercises

Exercise 1: Plugin System

Difficulty: Intermediate | Time: 25-30 minutes

Learning Objectives:

  • Master the Strategy pattern for extensible systems
  • Understand plugin architecture and dynamic loading
  • Practice building systems with pluggable components

Real-World Context:
Plugin systems are essential in modern software architecture. They're used in IDEs like VS Code for extensions, in CMS platforms like WordPress for functionality modules, in build tools like Maven for custom tasks, and in monitoring systems for custom integrations. Understanding plugin architecture will help you build extensible applications that can grow and adapt without requiring core changes.

Task:
Implement a plugin system using the Strategy pattern that allows dynamic registration and execution of plugins with a common interface, supporting plugin discovery, execution with parameters, and error handling.

Solution
 1package main
 2
 3import "fmt"
 4
 5type Plugin interface {
 6    Name() string
 7    Execute(args map[string]string) string
 8}
 9
10type PluginManager struct {
11    plugins map[string]Plugin
12}
13
14func NewPluginManager() *PluginManager {
15    return &PluginManager{
16        plugins: make(map[string]Plugin),
17    }
18}
19
20func (pm *PluginManager) Register(plugin Plugin) {
21    pm.plugins[plugin.Name()] = plugin
22}
23
24func (pm *PluginManager) Execute(name string, args map[string]string) (string, error) {
25    plugin, exists := pm.plugins[name]
26    if !exists {
27        return "", fmt.Errorf("plugin %s not found", name)
28    }
29
30    return plugin.Execute(args), nil
31}
32
33type GreetPlugin struct{}
34
35func (GreetPlugin) Name() string {
36    return "greet"
37}
38
39func (GreetPlugin) Execute(args map[string]string) string {
40    name := args["name"]
41    if name == "" {
42        name = "World"
43    }
44    return fmt.Sprintf("Hello, %s!", name)
45}
46
47type MathPlugin struct{}
48
49func (MathPlugin) Name() string {
50    return "math"
51}
52
53func (MathPlugin) Execute(args map[string]string) string {
54    operation := args["operation"]
55    return fmt.Sprintf("Math operation: %s", operation)
56}
57
58func main() {
59    manager := NewPluginManager()
60
61    manager.Register(GreetPlugin{})
62    manager.Register(MathPlugin{})
63
64    result, _ := manager.Execute("greet", map[string]string{"name": "Alice"})
65    fmt.Println(result)
66
67    result, _ = manager.Execute("math", map[string]string{"operation": "add"})
68    fmt.Println(result)
69}

Exercise 2: Event Bus

Difficulty: Intermediate | Time: 25-30 minutes

Learning Objectives:

  • Master the Observer pattern for decoupled communication
  • Understand publish-subscribe architecture
  • Practice building thread-safe event systems

Real-World Context:
Event buses are fundamental in modern application architecture. They're used in microservices for inter-service communication, in GUI applications for user interface events, in message queue systems for asynchronous processing, and in real-time systems for notifications. Understanding event-driven architecture will help you build loosely coupled, scalable systems that can handle complex workflows and distributed communication.

Task:
Implement a publish-subscribe event bus that supports topic-based subscription, async event delivery, and proper cleanup of subscribers, handling concurrent access and ensuring message delivery reliability.

Solution
 1package main
 2
 3import (
 4    "fmt"
 5    "sync"
 6)
 7
 8type EventHandler func(data interface{})
 9
10type EventBus struct {
11    mu       sync.RWMutex
12    handlers map[string][]EventHandler
13}
14
15func NewEventBus() *EventBus {
16    return &EventBus{
17        handlers: make(map[string][]EventHandler),
18    }
19}
20
21func (eb *EventBus) Subscribe(event string, handler EventHandler) {
22    eb.mu.Lock()
23    defer eb.mu.Unlock()
24
25    eb.handlers[event] = append(eb.handlers[event], handler)
26}
27
28func (eb *EventBus) Publish(event string, data interface{}) {
29    eb.mu.RLock()
30    handlers := eb.handlers[event]
31    eb.mu.RUnlock()
32
33    for _, handler := range handlers {
34        go handler(data)
35    }
36}
37
38func main() {
39    bus := NewEventBus()
40
41    // Subscribe to user.created event
42    bus.Subscribe("user.created", func(data interface{}) {
43        fmt.Printf("Handler 1: User created - %v\n", data)
44    })
45
46    bus.Subscribe("user.created", func(data interface{}) {
47        fmt.Printf("Handler 2: Sending welcome email to %v\n", data)
48    })
49
50    // Subscribe to user.deleted event
51    bus.Subscribe("user.deleted", func(data interface{}) {
52        fmt.Printf("User deleted - %v\n", data)
53    })
54
55    // Publish events
56    bus.Publish("user.created", map[string]string{"name": "Alice", "email": "alice@example.com"})
57    bus.Publish("user.deleted", map[string]string{"name": "Bob"})
58
59    // Give time for async handlers
60    fmt.Scanln()
61}

Exercise 3: Middleware Chain

Difficulty: Advanced | Time: 30-35 minutes

Learning Objectives:

  • Master the Decorator pattern for request processing
  • Understand middleware composition and chaining
  • Practice building modular request processing pipelines

Real-World Context:
Middleware chains are essential in modern web applications. They're used in Express.js and similar frameworks for request processing, in API gateways for cross-cutting concerns, in authentication systems for layered security, and in logging systems for request tracing. Understanding middleware patterns will help you build clean, modular applications with separation of concerns and reusable request processing components.

Task:
Implement an HTTP middleware chain pattern that allows composition of multiple middleware functions, supporting authentication, logging, error handling, and request/response modification with proper error propagation and cleanup.

Solution
 1package main
 2
 3import (
 4    "fmt"
 5    "log"
 6    "net/http"
 7    "time"
 8)
 9
10type Middleware func(http.HandlerFunc) http.HandlerFunc
11
12func Chain(f http.HandlerFunc, middlewares ...Middleware) http.HandlerFunc {
13    for i := len(middlewares) - 1; i >= 0; i-- {
14        f = middlewares[i](f)
15    }
16    return f
17}
18
19func Logging() Middleware {
20    return func(next http.HandlerFunc) http.HandlerFunc {
21        return func(w http.ResponseWriter, r *http.Request) {
22            start := time.Now()
23            log.Printf("Started %s %s", r.Method, r.URL.Path)
24            next(w, r)
25            log.Printf("Completed in %v", time.Since(start))
26        }
27    }
28}
29
30func Auth() Middleware {
31    return func(next http.HandlerFunc) http.HandlerFunc {
32        return func(w http.ResponseWriter, r *http.Request) {
33            token := r.Header.Get("Authorization")
34            if token != "secret" {
35                http.Error(w, "Unauthorized", http.StatusUnauthorized)
36                return
37            }
38            next(w, r)
39        }
40    }
41}
42
43func Recovery() Middleware {
44    return func(next http.HandlerFunc) http.HandlerFunc {
45        return func(w http.ResponseWriter, r *http.Request) {
46            defer func() {
47                if err := recover(); err != nil {
48                    log.Printf("Panic: %v", err)
49                    http.Error(w, "Internal Server Error", http.StatusInternalServerError)
50                }
51            }()
52            next(w, r)
53        }
54    }
55}
56
57func helloHandler(w http.ResponseWriter, r *http.Request) {
58    fmt.Fprint(w, "Hello, World!")
59}
60
61func main() {
62    handler := Chain(
63        helloHandler,
64        Logging(),
65        Auth(),
66        Recovery(),
67    )
68
69    http.HandleFunc("/", handler)
70    log.Println("Server starting on :8080")
71    http.ListenAndServe(":8080", nil)
72}

Exercise 4: Object Pool Pattern

Difficulty: Advanced | Time: 40-45 minutes

Learning Objectives:

  • Master resource management and lifecycle patterns
  • Understand pool sizing and connection management
  • Practice building concurrent-safe resource pools

Real-World Context:
Object pools are critical for performance optimization in high-load applications. They're used in database connection pooling for efficient resource usage, in HTTP client pools for connection reuse, in thread pools for concurrent task execution, and in memory allocation for high-frequency operations. Understanding object pooling will help you build scalable applications that efficiently manage expensive resources while maintaining performance under load.

Task:
Implement an object pool for managing expensive resources like database connections, with automatic cleanup, health checking, dynamic sizing, and concurrent access while handling resource exhaustion and recovery scenarios.

Solution
  1package main
  2
  3import (
  4	"errors"
  5	"fmt"
  6	"sync"
  7	"time"
  8)
  9
 10// Resource represents a pooled resource
 11type Resource struct {
 12	ID        int
 13	CreatedAt time.Time
 14	LastUsed  time.Time
 15	InUse     bool
 16	mu        sync.Mutex
 17}
 18
 19func (r *Resource) Use() {
 20	r.mu.Lock()
 21	defer r.mu.Unlock()
 22	r.InUse = true
 23	r.LastUsed = time.Now()
 24}
 25
 26func (r *Resource) Release() {
 27	r.mu.Lock()
 28	defer r.mu.Unlock()
 29	r.InUse = false
 30}
 31
 32func (r *Resource) IsHealthy() bool {
 33	// Check if resource is too old
 34	return time.Since(r.CreatedAt) < 5*time.Minute
 35}
 36
 37// ObjectPool manages a pool of reusable resources
 38type ObjectPool struct {
 39	resources   []*Resource
 40	available   chan *Resource
 41	maxSize     int
 42	idleTimeout time.Duration
 43	mu          sync.Mutex
 44	counter     int
 45	stopCh      chan struct{}
 46}
 47
 48func NewObjectPool(initialSize, maxSize int, idleTimeout time.Duration) *ObjectPool {
 49	pool := &ObjectPool{
 50		resources:   make([]*Resource, 0, maxSize),
 51		available:   make(chan *Resource, maxSize),
 52		maxSize:     maxSize,
 53		idleTimeout: idleTimeout,
 54		stopCh:      make(chan struct{}),
 55	}
 56
 57	// Create initial resources
 58	for i := 0; i < initialSize; i++ {
 59		pool.createResource()
 60	}
 61
 62	// Start cleanup goroutine
 63	go pool.cleanup()
 64
 65	return pool
 66}
 67
 68func (p *ObjectPool) createResource() *Resource {
 69	p.mu.Lock()
 70	defer p.mu.Unlock()
 71
 72	if len(p.resources) >= p.maxSize {
 73		return nil
 74	}
 75
 76	p.counter++
 77	resource := &Resource{
 78		ID:        p.counter,
 79		CreatedAt: time.Now(),
 80		LastUsed:  time.Now(),
 81	}
 82
 83	p.resources = append(p.resources, resource)
 84	p.available <- resource
 85
 86	fmt.Printf("Created resource #%d (total: %d)\n", resource.ID, len(p.resources))
 87	return resource
 88}
 89
 90// Acquire gets a resource from the pool
 91func (p *ObjectPool) Acquire() (*Resource, error) {
 92	select {
 93	case resource := <-p.available:
 94		// Check if resource is healthy
 95		if !resource.IsHealthy() {
 96			fmt.Printf("Resource #%d is unhealthy, creating new one\n", resource.ID)
 97			p.removeResource(resource)
 98			return p.Acquire() // Try again
 99		}
100
101		resource.Use()
102		fmt.Printf("Acquired resource #%d\n", resource.ID)
103		return resource, nil
104
105	case <-time.After(5 * time.Second):
106		// No resource available, try to create new one
107		resource := p.createResource()
108		if resource != nil {
109			resource.Use()
110			fmt.Printf("Created and acquired resource #%d\n", resource.ID)
111			return resource, nil
112		}
113		return nil, errors.New("pool exhausted: max size reached")
114	}
115}
116
117// Release returns a resource to the pool
118func (p *ObjectPool) Release(resource *Resource) {
119	resource.Release()
120	fmt.Printf("Released resource #%d\n", resource.ID)
121
122	// Return to pool
123	select {
124	case p.available <- resource:
125		// Successfully returned to pool
126	default:
127		// Channel full, discard resource
128		p.removeResource(resource)
129	}
130}
131
132func (p *ObjectPool) removeResource(resource *Resource) {
133	p.mu.Lock()
134	defer p.mu.Unlock()
135
136	for i, r := range p.resources {
137		if r.ID == resource.ID {
138			p.resources = append(p.resources[:i], p.resources[i+1:]...)
139			fmt.Printf("Removed resource #%d (total: %d)\n", resource.ID, len(p.resources))
140			break
141		}
142	}
143}
144
145// cleanup removes idle resources periodically
146func (p *ObjectPool) cleanup() {
147	ticker := time.NewTicker(30 * time.Second)
148	defer ticker.Stop()
149
150	for {
151		select {
152		case <-ticker.C:
153			p.cleanupIdle()
154		case <-p.stopCh:
155			return
156		}
157	}
158}
159
160func (p *ObjectPool) cleanupIdle() {
161	p.mu.Lock()
162	defer p.mu.Unlock()
163
164	now := time.Now()
165	for i := len(p.resources) - 1; i >= 0; i-- {
166		resource := p.resources[i]
167		if !resource.InUse && now.Sub(resource.LastUsed) > p.idleTimeout {
168			// Remove idle resource
169			select {
170			case <-p.available:
171				// Successfully removed from channel
172			default:
173			}
174			p.resources = append(p.resources[:i], p.resources[i+1:]...)
175			fmt.Printf("Cleaned up idle resource #%d\n", resource.ID)
176		}
177	}
178}
179
180func (p *ObjectPool) Stats() (total, available, inUse int) {
181	p.mu.Lock()
182	defer p.mu.Unlock()
183
184	total = len(p.resources)
185	available = len(p.available)
186	inUse = total - available
187
188	return
189}
190
191func (p *ObjectPool) Close() {
192	close(p.stopCh)
193	close(p.available)
194}
195
196func main() {
197	// Create pool with initial size 2, max size 5, idle timeout 10s
198	pool := NewObjectPool(2, 5, 10*time.Second)
199	defer pool.Close()
200
201	fmt.Println("\n=== Testing pool acquisition ===")
202
203	// Acquire multiple resources
204	resources := make([]*Resource, 0)
205	for i := 0; i < 3; i++ {
206		resource, err := pool.Acquire()
207		if err != nil {
208			fmt.Printf("Error acquiring resource: %v\n", err)
209			continue
210		}
211		resources = append(resources, resource)
212	}
213
214	// Show stats
215	total, available, inUse := pool.Stats()
216	fmt.Printf("\nPool stats: Total=%d, Available=%d, InUse=%d\n", total, available, inUse)
217
218	// Release resources
219	fmt.Println("\n=== Releasing resources ===")
220	for _, resource := range resources {
221		pool.Release(resource)
222		time.Sleep(500 * time.Millisecond)
223	}
224
225	total, available, inUse = pool.Stats()
226	fmt.Printf("\nPool stats: Total=%d, Available=%d, InUse=%d\n", total, available, inUse)
227
228	// Simulate concurrent access
229	fmt.Println("\n=== Testing concurrent access ===")
230	var wg sync.WaitGroup
231	for i := 0; i < 10; i++ {
232		wg.Add(1)
233		go func(id int) {
234			defer wg.Done()
235
236			resource, err := pool.Acquire()
237			if err != nil {
238				fmt.Printf("Worker %d: Error: %v\n", id, err)
239				return
240			}
241
242			// Simulate work
243			time.Sleep(500 * time.Millisecond)
244			pool.Release(resource)
245		}(i)
246	}
247
248	wg.Wait()
249
250	total, available, inUse = pool.Stats()
251	fmt.Printf("\nFinal pool stats: Total=%d, Available=%d, InUse=%d\n", total, available, inUse)
252}

Key Features:

  • Dynamic pool size with min/max limits
  • Resource health checking
  • Automatic cleanup of idle resources
  • Thread-safe with mutex and channels
  • Handles pool exhaustion gracefully
  • Concurrent worker support
  • Statistics tracking

Exercise 5: Fluent Builder Pattern

Difficulty: Expert | Time: 45-50 minutes

Learning Objectives:

  • Master the Builder pattern for complex object construction
  • Understand fluent interface design and method chaining
  • Practice building flexible, readable configuration APIs

Real-World Context:
Fluent builders are essential for creating clean, maintainable APIs. They're used in HTTP client libraries like Retrofit for request building, in database query builders for dynamic SQL generation, in configuration systems for complex object setup, and in testing frameworks for test data construction. Understanding fluent interfaces will help you design APIs that are both powerful and pleasant to use, reducing the cognitive load for developers working with your code.

Task:
Implement a fluent builder for constructing complex HTTP requests with method chaining, allowing for clean, readable configuration while supporting validation, default values, and error handling throughout the building process.

Solution
  1package main
  2
  3import (
  4	"bytes"
  5	"encoding/json"
  6	"fmt"
  7	"io"
  8	"net/http"
  9	"net/url"
 10	"strings"
 11	"time"
 12)
 13
 14// HTTPRequestBuilder builds HTTP requests using method chaining
 15type HTTPRequestBuilder struct {
 16	method  string
 17	url     string
 18	headers map[string]string
 19	params  url.Values
 20	body    interface{}
 21	timeout time.Duration
 22	err     error
 23}
 24
 25// NewRequest creates a new request builder
 26func NewRequest() *HTTPRequestBuilder {
 27	return &HTTPRequestBuilder{
 28		method:  "GET",
 29		headers: make(map[string]string),
 30		params:  make(url.Values),
 31		timeout: 30 * time.Second,
 32	}
 33}
 34
 35// Method sets the HTTP method
 36func (b *HTTPRequestBuilder) Method(method string) *HTTPRequestBuilder {
 37	b.method = strings.ToUpper(method)
 38	return b
 39}
 40
 41// GET is a shorthand for Method("GET")
 42func (b *HTTPRequestBuilder) GET() *HTTPRequestBuilder {
 43	return b.Method("GET")
 44}
 45
 46// POST is a shorthand for Method("POST")
 47func (b *HTTPRequestBuilder) POST() *HTTPRequestBuilder {
 48	return b.Method("POST")
 49}
 50
 51// PUT is a shorthand for Method("PUT")
 52func (b *HTTPRequestBuilder) PUT() *HTTPRequestBuilder {
 53	return b.Method("PUT")
 54}
 55
 56// DELETE is a shorthand for Method("DELETE")
 57func (b *HTTPRequestBuilder) DELETE() *HTTPRequestBuilder {
 58	return b.Method("DELETE")
 59}
 60
 61// URL sets the request URL
 62func (b *HTTPRequestBuilder) URL(url string) *HTTPRequestBuilder {
 63	b.url = url
 64	return b
 65}
 66
 67// Header adds a header
 68func (b *HTTPRequestBuilder) Header(key, value string) *HTTPRequestBuilder {
 69	b.headers[key] = value
 70	return b
 71}
 72
 73// Headers sets multiple headers at once
 74func (b *HTTPRequestBuilder) Headers(headers map[string]string) *HTTPRequestBuilder {
 75	for k, v := range headers {
 76		b.headers[k] = v
 77	}
 78	return b
 79}
 80
 81// ContentType sets the Content-Type header
 82func (b *HTTPRequestBuilder) ContentType(contentType string) *HTTPRequestBuilder {
 83	return b.Header("Content-Type", contentType)
 84}
 85
 86// Accept sets the Accept header
 87func (b *HTTPRequestBuilder) Accept(accept string) *HTTPRequestBuilder {
 88	return b.Header("Accept", accept)
 89}
 90
 91// BearerToken sets Authorization header with Bearer token
 92func (b *HTTPRequestBuilder) BearerToken(token string) *HTTPRequestBuilder {
 93	return b.Header("Authorization", "Bearer "+token)
 94}
 95
 96// BasicAuth sets Authorization header with Basic auth
 97func (b *HTTPRequestBuilder) BasicAuth(username, password string) *HTTPRequestBuilder {
 98	auth := username + ":" + password
 99	encodedAuth := base64Encode(auth)
100	return b.Header("Authorization", "Basic "+encodedAuth)
101}
102
103// Param adds a query parameter
104func (b *HTTPRequestBuilder) Param(key, value string) *HTTPRequestBuilder {
105	b.params.Add(key, value)
106	return b
107}
108
109// Params sets multiple query parameters at once
110func (b *HTTPRequestBuilder) Params(params map[string]string) *HTTPRequestBuilder {
111	for k, v := range params {
112		b.params.Add(k, v)
113	}
114	return b
115}
116
117// Body sets the request body
118func (b *HTTPRequestBuilder) Body(body interface{}) *HTTPRequestBuilder {
119	b.body = body
120	return b
121}
122
123// JSON sets the body as JSON and Content-Type header
124func (b *HTTPRequestBuilder) JSON(data interface{}) *HTTPRequestBuilder {
125	b.body = data
126	return b.ContentType("application/json")
127}
128
129// Timeout sets the request timeout
130func (b *HTTPRequestBuilder) Timeout(timeout time.Duration) *HTTPRequestBuilder {
131	b.timeout = timeout
132	return b
133}
134
135// Build creates the http.Request
136func (b *HTTPRequestBuilder) Build() (*http.Request, error) {
137	if b.err != nil {
138		return nil, b.err
139	}
140
141	if b.url == "" {
142		return nil, fmt.Errorf("URL is required")
143	}
144
145	// Build URL with query parameters
146	fullURL := b.url
147	if len(b.params) > 0 {
148		if strings.Contains(fullURL, "?") {
149			fullURL += "&" + b.params.Encode()
150		} else {
151			fullURL += "?" + b.params.Encode()
152		}
153	}
154
155	// Build body
156	var bodyReader io.Reader
157	if b.body != nil {
158		if str, ok := b.body.(string); ok {
159			bodyReader = strings.NewReader(str)
160		} else {
161			// Assume JSON
162			jsonData, err := json.Marshal(b.body)
163			if err != nil {
164				return nil, fmt.Errorf("failed to marshal body: %w", err)
165			}
166			bodyReader = bytes.NewReader(jsonData)
167		}
168	}
169
170	// Create request
171	req, err := http.NewRequest(b.method, fullURL, bodyReader)
172	if err != nil {
173		return nil, err
174	}
175
176	// Set headers
177	for key, value := range b.headers {
178		req.Header.Set(key, value)
179	}
180
181	return req, nil
182}
183
184// Execute builds and executes the request
185func (b *HTTPRequestBuilder) Execute() (*http.Response, error) {
186	req, err := b.Build()
187	if err != nil {
188		return nil, err
189	}
190
191	client := &http.Client{
192		Timeout: b.timeout,
193	}
194
195	return client.Do(req)
196}
197
198// Simple base64 encoding
199func base64Encode(s string) string {
200	// In production, use encoding/base64
201	return s // Simplified for example
202}
203
204func main() {
205	fmt.Println("=== Example 1: Simple GET request ===")
206	req1, _ := NewRequest().
207		GET().
208		URL("https://api.example.com/users").
209		Param("page", "1").
210		Param("limit", "10").
211		Header("User-Agent", "MyApp/1.0").
212		Build()
213
214	fmt.Printf("Method: %s\n", req1.Method)
215	fmt.Printf("URL: %s\n", req1.URL.String())
216	fmt.Printf("Headers: %v\n\n", req1.Header)
217
218	fmt.Println("=== Example 2: POST with JSON ===")
219	userData := map[string]interface{}{
220		"name":  "John Doe",
221		"email": "john@example.com",
222		"age":   30,
223	}
224
225	req2, _ := NewRequest().
226		POST().
227		URL("https://api.example.com/users").
228		JSON(userData).
229		BearerToken("abc123xyz").
230		Timeout(10 * time.Second).
231		Build()
232
233	fmt.Printf("Method: %s\n", req2.Method)
234	fmt.Printf("URL: %s\n", req2.URL.String())
235	fmt.Printf("Content-Type: %s\n", req2.Header.Get("Content-Type"))
236	fmt.Printf("Authorization: %s\n\n", req2.Header.Get("Authorization"))
237
238	fmt.Println("=== Example 3: Complex request with multiple options ===")
239	req3, _ := NewRequest().
240		PUT().
241		URL("https://api.example.com/users/123").
242		Headers(map[string]string{
243			"X-Request-ID": "req-12345",
244			"X-API-Version": "v2",
245		}).
246		Params(map[string]string{
247			"force": "true",
248			"notify": "false",
249		}).
250		JSON(map[string]string{
251			"status": "active",
252		}).
253		ContentType("application/json").
254		Accept("application/json").
255		Timeout(5 * time.Second).
256		Build()
257
258	fmt.Printf("Method: %s\n", req3.Method)
259	fmt.Printf("URL: %s\n", req3.URL.String())
260	fmt.Printf("Headers: %v\n", req3.Header)
261
262	fmt.Println("\n=== Example 4: Builder with Execute ===")
263	// This would actually make the request in production
264	builder := NewRequest().
265		GET().
266		URL("https://api.github.com/users/octocat").
267		Header("User-Agent", "Go-Client").
268		Timeout(5 * time.Second)
269
270	fmt.Printf("Builder configured for: %s %s\n", builder.method, builder.url)
271	// resp, err := builder.Execute() // Uncomment to actually execute
272}

Output:

=== Example 1: Simple GET request ===
Method: GET
URL: https://api.example.com/users?page=1&limit=10
Headers: map[User-Agent:[MyApp/1.0]]

=== Example 2: POST with JSON ===
Method: POST
URL: https://api.example.com/users
Content-Type: application/json
Authorization: Bearer abc123xyz

=== Example 3: Complex request with multiple options ===
Method: PUT
URL: https://api.example.com/users/123?force=true&notify=false
Headers: map[Accept:[application/json] Content-Type:[application/json] X-Api-Version:[v2] X-Request-Id:[req-12345]]

=== Example 4: Builder with Execute ===
Builder configured for: GET https://api.github.com/users/octocat

Key Features:

  • Fluent interface with method chaining
  • Shorthand methods for common HTTP verbs
  • Automatic query parameter encoding
  • JSON body marshaling
  • Bearer and Basic authentication helpers
  • Customizable timeout
  • Multiple ways to set headers and params
  • Clean, readable API
  • Error propagation through the chain

Summary

Design patterns are powerful tools in your Go programming arsenal. They provide proven solutions to common problems, but they should be applied judiciously. Always remember:

  1. Understand the problem first - Don't start with a pattern
  2. Choose the simplest solution - Patterns add complexity
  3. Leverage Go's strengths - Use interfaces, composition, and channels
  4. Keep it idiomatic - Write Go, not Java/C++/Python in Go
  5. Refactor to patterns - Don't start with them
  6. Test thoroughly - Patterns don't guarantee correctness
  7. Document your choices - Explain why you chose a pattern

The best code is code that's easy to understand, maintain, and extend. Patterns help achieve these goals when applied appropriately. Master these patterns, understand their trade-offs, and build production-ready Go applications with confidence.