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:
-
Creational Patterns: Control object creation mechanisms
- Singleton, Factory, Builder, Prototype, Object Pool
-
Structural Patterns: Compose objects into larger structures
- Adapter, Decorator, Composite, Facade, Proxy, Bridge, Flyweight
-
Behavioral Patterns: Define communication between objects
- Strategy, Observer, Command, Iterator, State, Template Method, Chain of Responsibility
Go's Unique Fourth Category:
- 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:
- Testing difficulties - Singletons make unit testing harder; consider dependency injection
- Hidden dependencies - Global state can obscure dependencies between components
- 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:
- Too many options: If you have more than 10-15 options, consider breaking the type into smaller components
- Complex validation: Put validation in a separate
Validate()method, not in individual options - 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:
- Overly general design: Don't make the base interface too complex—only include operations that make sense for both leaf and composite objects
- Type safety: Be careful with type assertions—use interfaces appropriately to maintain the pattern's benefits
- 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:
- Over-engineering: Don't use Command pattern for simple method calls. It's most valuable when you need to queue, undo, or parameterize commands.
- Memory leaks: Be careful with command queues that grow indefinitely. Implement size limits and proper cleanup.
- 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:
- The problem matches the pattern's intent
- Benefits outweigh complexity
- Team understands the pattern
- Future changes are anticipated
Don't use patterns when:
- Simple solution exists
- Problem doesn't match pattern
- Over-engineering for current needs
- 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
- Favor composition over inheritance - Use embedding
- Keep interfaces small - Easier to implement and test
- Accept interfaces, return structs - More flexible APIs
- Use functional options for complex constructors - Clean and extensible
- Leverage goroutines and channels - For concurrent patterns
- Don't overuse patterns - Keep code simple and readable
- Prefer standard library patterns - io.Reader, io.Writer, etc.
- CQRS for complex domains - Separate reads and writes when scalability matters
- Event sourcing for audit trails - Store all state changes as events
- 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¬ify=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:
- Understand the problem first - Don't start with a pattern
- Choose the simplest solution - Patterns add complexity
- Leverage Go's strengths - Use interfaces, composition, and channels
- Keep it idiomatic - Write Go, not Java/C++/Python in Go
- Refactor to patterns - Don't start with them
- Test thoroughly - Patterns don't guarantee correctness
- 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.