Design patterns are recurring solutions to common problems in software design. The Gang of Four (GoF) categorizes these into three main types: Creational, Structural, and Behavioral. In this post, we explore real-world implementations using Go, focusing on clarity, simplicity, and idiomatic code. Whether you're building microservices or complex monoliths, understanding these patterns helps you write maintainable, scalable, and testable code.
Creational Patterns: Object Creation
Creational patterns deal with object creation mechanisms, trying to create objects in a manner suitable to the situation. The most common is the Factory Method.
Factory Method in Go
In Go, interface-based factory methods are idiomatic. Consider a payment system supporting multiple processors:
type PaymentProcessor interface {
Process(amount float64) error
}
type StripeProcessor struct{}
func (s StripeProcessor) Process(amount float64) error {
fmt.Printf("Processing $%.2f via Stripe\n", amount)
return nil
}
type PayPalProcessor struct{}
func (p PayPalProcessor) Process(amount float64) error {
fmt.Printf("Processing $%.2f via PayPal\n", amount)
return nil
}
func NewPaymentProcessor(provider string) PaymentProcessor {
switch provider {
case "stripe":
return StripeProcessor{}
case "paypal":
return PayPalProcessor{}
default:
return nil
}
}
This decouples client code from concrete implementations. You can easily add new payment methods without modifying existing code, adhering to the Open/Closed Principle.
Structural Patterns: Object Composition
Structural patterns deal with how classes and objects are composed to form larger structures. The Adapter Pattern is crucial for integrating incompatible interfaces.
Adapter Pattern in Go
Suppose you have a legacy logging interface that differs from your modern application's logger:
type LegacyLogger interface {
Log(msg string)
}
type AppLogger interface {
Info(msg string)
Error(msg string)
}
type LoggerAdapter struct {
legacy LegacyLogger
}
func (a LoggerAdapter) Info(msg string) {
a.legacy.Log("[INFO] " + msg)
}
func (a LoggerAdapter) Error(msg string) {
a.legacy.Log("[ERROR] " + msg)
}
func NewLoggerAdapter(legacy LegacyLogger) AppLogger {
return LoggerAdapter{legacy: legacy}
}
This adapter allows your modern application to use the legacy logger without changing its interface, promoting code reuse and gradual refactoring.
Behavioral Patterns: Object Interaction
Behavioral patterns are concerned with algorithms and the assignment of responsibilities between objects. The Observer Pattern is vital for event-driven architectures.
Observer Pattern in Go
Go's channels provide a natural implementation of the Observer pattern for async communication:
type Event struct {
ID int
Data string
}
type EventHandler func(Event)
type EventBus struct {
handlers map[string][]EventHandler
}
func NewEventBus() *EventBus {
return &EventBus{handlers: make(map[string][]EventHandler)}
}
func (eb *EventBus) Subscribe(eventType string, handler EventHandler) {
eb.handlers[eventType] = append(eb.handlers[eventType], handler)
}
func (eb *EventBus) Publish(event Event) {
for _, handler := range eb.handlers["order"] {
go handler(event)
}
}
func main() {
bus := NewEventBus()
bus.Subscribe("order", func(e Event) {
fmt.Printf("Email sent for order %d\n", e.ID)
})
bus.Subscribe("order", func(e Event) {
fmt.Printf("Notification sent for order %d\n", e.ID)
})
bus.Publish(Event{ID: 1, Data: "New Order"})
}
This loose coupling allows multiple subscribers to react to events without knowing about each other, ideal for microservices and event-driven systems.
Conclusion
Design patterns are not one-size-fits-all solutions but rather proven templates that address common architectural challenges. Creational patterns simplify object creation, structural patterns manage relationships between objects, and behavioral patterns handle communication. In Go, leverage the language's strengths like interfaces and channels to implement these patterns idiomatically. Remember: choose patterns that solve your specific problem, and don't force them into places where they don't fit. Mastering these patterns will elevate your code from functional to robust and maintainable.