Best Practices for Design Patterns in Go
Daniel Hayes
Full-Stack Engineer ยท Leapcell

Implementation of Ten Design Patterns in Go Language and Their Applications in Internet Scenarios
1. Singleton Pattern
Pattern Definition
Ensure that only one instance of a class is created globally, and provide a unified access point. It belongs to the creational pattern and is suitable for scenarios where global unique control is required.
Core Features
- Unique Instance: There is only one object instance globally.
- Self-initialization: The class itself is responsible for creating the instance.
- Global Access: Provide an access entry through a static method or a global variable.
Advantages and Disadvantages
Advantages | Disadvantages |
---|---|
Ensure global uniqueness of resources/state | Violate the principle of high cohesion, with high module coupling |
Reduce repeated creation of resources | Difficult to isolate unit tests |
Provide a convenient global access point | Damage the dependency injection pattern |
Scenario Applications
- Microservice Configuration Center (such as an alternative to Spring Cloud Config): Manage the configuration of distributed systems uniformly.
- Database Connection Pool (such as connection management for PostgreSQL/MySQL): Control the number of concurrent connections.
- Distributed Log Service (such as the log collector in the ELK Stack): Avoid repeated creation of log handlers.
- API Rate Limiter (such as the request frequency control of the Stripe API): Share the rate limiting state globally.
Go Language Implementation (Concurrency-safe Version)
package singleton import ( "sync" ) // ConfigManager Singleton struct, simulating configuration management type ConfigManager struct { AppConfig map[string]string } var ( once sync.Once instance *ConfigManager ) // GetInstance Global access point, using sync.Once to ensure concurrency safety func GetInstance() *ConfigManager { once.Do(func() { instance = &ConfigManager{ AppConfig: map[string]string{ "env": "prod", "port": "8080", "timeout": "30s", }, } }) return instance } // Concurrency test example func TestConcurrency() { var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) go func() { defer wg.Done() config := GetInstance() println(config.AppConfig["env"]) }() } wg.Wait() }
2. Factory Pattern
Pattern Definition
Encapsulate the object creation logic, and determine the specific product to be instantiated through subclasses. It belongs to the creational pattern and decouples the object creation from its usage.
Core Features
- Encapsulation of Creation Logic: The client does not need to know the specific creation details.
- Polymorphism Support: Extend the product family through interfaces.
- Open-Closed Principle: Adding new products does not require modifying the existing code.
Advantages and Disadvantages
Advantages | Disadvantages |
---|---|
Separate object creation from usage | Increase the number of classes (each product needs a corresponding factory) |
Easy to extend new products | The factory class may become too complex |
Comply with the Dependency Inversion Principle | The client needs to know the abstract interface of the product |
Scenario Applications
- Payment Gateway Integration (Stripe/PayPal/Apple Pay): Create different processors according to the payment method.
- Cloud Storage Client (S3/GCS/Azure Blob): Create corresponding API clients according to the storage service.
- Message Queue Adapter (Kafka/RabbitMQ/NATS): Unify the creation logic of the message sending interface.
- Third-party Login Service (OAuth2/OpenID Connect): Dynamically generate authentication clients for different platforms.
Go Language Implementation (Abstract Factory Pattern)
package factory import "fmt" // PaymentProcessor Payment processor interface type PaymentProcessor interface { Process(amount float64) string } // StripeProcessor Stripe payment implementation type StripeProcessor struct{} func (s *StripeProcessor) Process(amount float64) string { return fmt.Sprintf("Stripe processed $%.2f", amount) } // PayPalProcessor PayPal payment implementation type PayPalProcessor struct{} func (p *PayPalProcessor) Process(amount float64) string { return fmt.Sprintf("PayPal processed $%.2f", amount) } // PaymentFactory Factory interface type PaymentFactory interface { CreateProcessor() PaymentProcessor } // StripeFactory Stripe factory implementation type StripeFactory struct{} func (s *StripeFactory) CreateProcessor() PaymentProcessor { return &StripeProcessor{} } // PayPalFactory PayPal factory implementation type PayPalFactory struct{} func (p *PayPalFactory) CreateProcessor() PaymentProcessor { return &PayPalProcessor{} } // Client usage example func NewPaymentClient(factory PaymentFactory) PaymentProcessor { return factory.CreateProcessor() }
3. Observer Pattern
Pattern Definition
Define a one-to-many dependency relationship between objects, and automatically notify all observers when the state of the subject changes. It belongs to the behavioral pattern and is suitable for event-driven scenarios.
Core Features
- Event-driven: The change of the subject's state triggers the update of the observers.
- Loose Coupling: The subject and the observers only interact through abstract interfaces.
- Dynamic Subscription: Observers can register or unregister at any time.
Advantages and Disadvantages
Advantages | Disadvantages |
---|---|
Support broadcast-style event notification | A large number of observers may lead to performance problems |
Easy to expand new observer types | Circular dependencies may cause memory leaks |
Comply with the Open-Closed Principle | The subject needs to maintain a list of observers |
Scenario Applications
- Real-time Notification System (Slack channel updates/Zoom meeting reminders): Users subscribe to the subject to receive real-time notifications.
- Stock Quotation Platform (Robinhood/Nasdaq real-time quotes): Investors subscribe to stock codes to get price changes.
- E-commerce Inventory Monitoring (Amazon product restocking notifications): Users subscribe to products to receive inventory change reminders.
- Distributed System Monitoring (Datadog metric alarms): The monitoring service subscribes to the state changes of microservices.
Go Language Implementation (Event-driven Model)
package observer import "fmt" // Observer Observer interface type Observer interface { Update(message string) } // Subject Subject interface type Subject interface { Attach(observer Observer) Detach(observer Observer) Notify(message string) } // StockTicker Specific subject: stock quotation type StockTicker struct { observers []Observer symbol string price float64 } func (s *StockTicker) Attach(observer Observer) { s.observers = append(s.observers, observer) } func (s *StockTicker) Detach(observer Observer) { for i, o := range s.observers { if o == observer { s.observers = append(s.observers[:i], s.observers[i+1:]...) return } } } func (s *StockTicker) Notify(message string) { for _, o := range s.observers { o.Update(message) } } // Investor Specific observer: investor type Investor struct { name string } func (i *Investor) Update(message string) { fmt.Printf("%s received: %s\n", i.name, message) }
4. Decorator Pattern
Pattern Definition
Dynamically add new functions to an object, and extend its responsibilities through composition rather than inheritance. It belongs to the structural pattern and is suitable for scenarios with functional layering.
Core Features
- Transparent Extension: Keep the interface consistent, and the client is unaware of it.
- Composition Feature: Support nesting of multiple layers of decorators.
- Runtime Binding: Dynamically determine the combination of object functions.
Advantages and Disadvantages
Advantages | Disadvantages |
---|---|
Avoid inheritance hierarchy explosion | Debugging may be difficult with multiple layers of decorators |
Support free combination of functions | Excessive use will increase the complexity of the system |
Comply with the Open-Closed Principle | The order of decoration may affect the execution result |
Scenario Applications
- API Middleware (Express.js/Koa.js middleware system): Stack functions such as logging, authentication, and rate limiting.
- E-commerce Order Processing (shipping fee calculation/discount offer/tax processing): Calculate the total order amount step by step.
- Cloud Storage Service (S3 encryption/compression/CDN acceleration): Dynamically combine storage functions.
- Microservice Gateway (Kong/NginX plugin system): Request verification, response transformation, and traffic monitoring.
Go Language Implementation (HTTP Middleware Simulation)
package decorator import "fmt" // Handler Basic processing function type Handler func(request string) string // LogDecorator Log decorator func LogDecorator(next Handler) Handler { return func(request string) string { fmt.Printf("Received request: %s\n", request) return next(request) } } // AuthDecorator Authentication decorator func AuthDecorator(next Handler) Handler { return func(request string) string { if request != "authenticated" { return "Unauthorized" } return next(request) } } // Basic processing function func BasicHandler(request string) string { return fmt.Sprintf("Processed: %s", request) } // Composite decorator example func CompositeHandler() Handler { return LogDecorator(AuthDecorator(BasicHandler)) }
5. Strategy Pattern
Pattern Definition
Encapsulate algorithms into independent strategy classes, and support dynamic switching at runtime. It belongs to the behavioral pattern and is suitable for scenarios with multiple algorithms.
Core Features
- Algorithm Encapsulation: Each strategy independently implements the algorithm.
- Strategy Switching: Dynamically select a strategy at runtime.
- Open-Closed Principle: Adding a new strategy does not require modifying the existing code.
Advantages and Disadvantages
Advantages | Disadvantages |
---|---|
Decouple the algorithm from the client | The client needs to know all strategy types |
Facilitate algorithm extension and replacement | The number of strategy classes may grow rapidly |
Support combined strategies | It is difficult to share the state between strategies |
Scenario Applications
- E-commerce Promotion Engine (full reduction/discount/gift strategies): Dynamically switch the calculation logic according to the activity type.
- Logistics Distribution System (FedEx/UPS/USPS shipping strategies): Calculate the shipping fee according to the user's choice.
- Data Sorting Service (sort by price/sales volume/rating): The front-end request determines the sorting strategy.
- Advertisement Placement Algorithm (precision marketing/geographical targeting/frequency control): Adjust the placement strategy in real time.
Go Language Implementation (E-commerce Promotion Calculation)
package strategy import "fmt" // PromotionStrategy Promotion strategy interface type PromotionStrategy interface { Calculate(amount float64) float64 } // PercentDiscount Percentage discount strategy type PercentDiscount struct { rate float64 // Discount rate (0.1 = 10%) } func (p *PercentDiscount) Calculate(amount float64) float64 { return amount * (1 - p.rate) } // FixedDiscount Fixed amount discount strategy type FixedDiscount struct { offset float64 // Fixed reduction amount } func (f *FixedDiscount) Calculate(amount float64) float64 { if amount > f.offset { return amount - f.offset } return amount } // CheckoutContext Checkout context type CheckoutContext struct { strategy PromotionStrategy } func (c *CheckoutContext) SetStrategy(strategy PromotionStrategy) { c.strategy = strategy } func (c *CheckoutContext) CalculateFinalAmount(amount float64) float64 { return c.strategy.Calculate(amount) }
6. Adapter Pattern
Pattern Definition
Convert incompatible interfaces so that classes with different interfaces can work together. It belongs to the structural pattern and is suitable for system integration scenarios.
Core Features
- Interface Conversion: Adapt the source interface to the target interface.
- Decouple the System: The client and the adaptee do not need to interact directly.
- Compatibility: Retain the original system code and add an adapter layer.
Advantages and Disadvantages
Advantages | Disadvantages |
---|---|
Solve the problem of interface incompatibility | Increase the complexity of the system's abstract layer |
Reuse legacy system code | Excessive use may make the system difficult to maintain |
Support two-way adaptation | May introduce performance overhead |
Scenario Applications
- Legacy System Migration (Docking a COBOL system with a microservice architecture): Adapt the old system API.
- Third-party Library Integration (Adapt the MongoDB driver to a SQL interface): Unify the data access layer.
- Cross-platform Development (Adapt iOS/Android native APIs to a unified interface): Mobile application framework.
- Cloud Service Docking (Adapt AWS SQS to the Kafka message format): Hybrid cloud architecture.
Go Language Implementation (Third-party Payment Adaptation)
package adapter import ( "errors" "fmt" ) // ThirdPartyPayment Third-party payment interface (source interface) type ThirdPartyPayment interface { ProcessPayment(orderID string, amount float64) error } // LegacyPayPal Legacy PayPal implementation (source implementation) type LegacyPayPal struct{} func (p *LegacyPayPal) ProcessPayment(orderID string, amount float64) error { fmt.Printf("Legacy PayPal processing order %s for $%.2f\n", orderID, amount) return nil } // TargetPayment Target payment interface (unified interface) type TargetPayment interface { Pay(orderID string, amountUSD float64) error } // PayPalAdapter Adapter implementation type PayPalAdapter struct { legacy *LegacyPayPal } func (a *PayPalAdapter) Pay(orderID string, amountUSD float64) error { // Convert the target interface to the source interface parameters return a.legacy.ProcessPayment(orderID, amountUSD) } // Example of handling incompatible situations func HandleIncompatiblePayment(payment ThirdPartyPayment) TargetPayment { return &PayPalAdapter{legacy: payment.(*LegacyPayPal)} }
7. Proxy Pattern
Pattern Definition
Provide a proxy layer for the target object to control access to it. It belongs to the structural pattern and is suitable for scenarios such as remote access and permission control.
Core Features
- Access Control: The proxy layer intercepts and processes requests.
- Lazy Loading: Create the target object on demand.
- Separation of Responsibilities: The proxy handles non-business logic (logging/security/caching).
Advantages and Disadvantages
Advantages | Disadvantages |
---|---|
Protect the target object | Increase the request processing delay |
Support distributed access | The proxy layer may become a single point of failure |
Implement lazy loading | The interfaces of the proxy class and the target class need to be consistent |
Scenario Applications
- API Gateway (Apigee/Postman API management): Proxy backend microservices and provide a unified entry.
- Cache Proxy (Redis/Memcached cache layer): Cache database query results.
- Permission Proxy (OAuth2 authorization server): Intercept requests and verify user permissions.
- Remote Proxy (gRPC remote service call): Hide the details of network communication.
Go Language Implementation (Cache Proxy)
package proxy import ( "sync" "time" ) // RealData Real data object (database query) type RealData struct { ID string Content string LastUpdate time.Time } func (r *RealData) FetchData() string { // Simulate database query delay time.Sleep(500 * time.Millisecond) return r.Content } // CacheProxy Cache proxy type CacheProxy struct { realData *RealData cache map[string]string mu sync.Mutex } func NewCacheProxy(id string, content string) *CacheProxy { return &CacheProxy{ realData: &RealData{ID: id, Content: content, LastUpdate: time.Now()}, cache: make(map[string]string), } } func (c *CacheProxy) FetchData() string { c.mu.Lock() defer c.mu.Unlock() if data, exists := c.cache[c.realData.ID]; exists { return data // Return the cached data directly } // First access, get the real data and cache it data := c.realData.FetchData() c.cache[c.realData.ID] = data return data }
8. Command Pattern
Pattern Definition
Encapsulate requests into command objects to decouple the request sender from the receiver. It belongs to the behavioral pattern and is suitable for scenarios that support undo and logging.
Core Features
- Request Encapsulation: The command object contains all the information required for execution.
- Transaction Support: Support batch execution and rollback.
- Logging: The execution history of commands can be persisted.
Advantages and Disadvantages
Advantages | Disadvantages |
---|---|
Support undo/redo operations | The number of command classes may increase significantly |
Decouple request sending and receiving | Complex command logic is difficult to maintain |
Support batch processing | Command parameters need to be determined in advance |
Scenario Applications
- Version Control System (Git commit operations): Each commit can be treated as a command that supports rollback.
- Database Transaction (Encapsulation of ACID operations): Combine multiple SQL commands to support commit/rollback.
- Task Scheduling System (Airflow task orchestration): Encapsulate distributed tasks into executable commands.
- Text Editor (Undo/Redo functions): Record editing operations as command objects.
Go Language Implementation (Database Transaction Simulation)
package command import ( "database/sql" "fmt" ) // DatabaseReceiver Database receiver type DatabaseReceiver struct { db *sql.DB } func (r *DatabaseReceiver) ExecuteSQL(sqlStmt string) error { fmt.Printf("Executing: %s\n", sqlStmt) return nil // Simulate SQL execution } func (r *DatabaseReceiver) Rollback() error { fmt.Println("Rolling back transaction") return nil // Simulate rollback } // Command Command interface type Command interface { Execute() error Undo() error } // InsertCommand Insert command type InsertCommand struct { receiver *DatabaseReceiver table string columns []string values []string prevValues map[string]string // Store old values for rollback } func (c *InsertCommand) Execute() error { // Execute the insert logic and record the old values c.prevValues = getPreviousValues(c.receiver, c.table, c.columns) return c.receiver.ExecuteSQL(fmt.Sprintf("INSERT INTO %s VALUES (%s)", c.table, c.values)) } func (c *InsertCommand) Undo() error { // Use prevValues to roll back the insert operation return c.receiver.Rollback() }
9. Composite Pattern
Pattern Definition
Combine objects into a tree structure and uniformly process individual objects (leaves) and composite objects (containers). It belongs to the structural pattern and is suitable for hierarchical management scenarios.
Core Features
- Tree Structure: Support part-whole hierarchical relationships.
- Unified Interface: The client treats leaves and containers consistently.
- Recursive Processing: Container objects can contain other containers or leaves.
Advantages and Disadvantages
Advantages | Disadvantages |
---|---|
Simplify hierarchical structure operations | Higher design complexity |
Support complex hierarchical traversal | The interfaces of leaves and containers need to be strictly unified |
Comply with the Open-Closed Principle | Function expansion may affect the overall structure |
Scenario Applications
- File System (AWS S3's Bucket/Object structure): Process files and folders uniformly.
- Organization Structure (Department/Employee management in an enterprise HR system): A department can contain sub-departments and employees.
- E-commerce Product Catalog (Amazon's category/subcategory/product hierarchy): Hierarchical product management.
- GUI Components (View/Text component nesting in React Native): Process the UI component tree uniformly.
Go Language Implementation (File System Simulation)
package composite import "fmt" // FileSystemNode File system node interface type FileSystemNode interface { List() string Add(node FileSystemNode) Remove(node FileSystemNode) } // File Leaf node: file type File struct { name string size int } func (f *File) List() string { return fmt.Sprintf("File: %s (%dKB)", f.name, f.size) } func (f *File) Add(node FileSystemNode) {} func (f *File) Remove(node FileSystemNode) {} // Directory Container node: directory type Directory struct { name string children []FileSystemNode } func (d *Directory) List() string { list := fmt.Sprintf("Directory: %s\n", d.name) for _, child := range d.children { list += fmt.Sprintf(" โโ %s\n", child.List()) } return list } func (d *Directory) Add(node FileSystemNode) { d.children = append(d.children, node) } func (d *Directory) Remove(node FileSystemNode) { for i, child := range d.children { if child == node { d.children = append(d.children[:i], d.children[i+1:]...) return } } }
10. Iterator Pattern
Pattern Definition
Provide a unified interface to traverse the elements of a collection and hide the internal data structure of the collection. It belongs to the behavioral pattern and is suitable for data traversal scenarios.
Core Features
- Abstract Traversal: The client does not need to know the internal implementation of the collection.
- Multiple Traversal Modes: Support traversal strategies such as forward, reverse, and filtering.
- Decouple the Collection: The collection and the iterator can evolve independently.
Advantages and Disadvantages
Advantages | Disadvantages |
---|---|
Unify the collection access interface | Increase the design cost of the iterator class |
Support traversal of complex data structures | Customizing iterators may lead to class bloat |
Comply with the Single Responsibility Principle | The iterator needs to maintain the traversal state |
Scenario Applications
- Big Data Processing (Traversal of data sets in Hadoop MapReduce): Process different storage formats uniformly.
- Database ORM (Traversal of result sets in GORM): Transparently handle the return formats of different databases.
- Graphic Rendering (Traversal of scene graph nodes in Three.js): Process the hierarchical structure of 3D objects uniformly.
- Log Analysis (Traversal of log entries in ELK): Support traversal of different log storage formats.
Go Language Implementation (Custom Collection Iterator)
package iterator import "fmt" // Collection Collection interface type Collection interface { CreateIterator() Iterator AddItem(item string) } // StringCollection String collection implementation type StringCollection struct { items []string } func (c *StringCollection) CreateIterator() Iterator { return &StringIterator{collection: c, index: -1} } func (c *StringCollection) AddItem(item string) { c.items = append(c.items, item) } // Iterator Iterator interface type Iterator interface { Next() bool Current() string } // StringIterator String iterator implementation type StringIterator struct { collection *StringCollection index int } func (i *StringIterator) Next() bool { i.index++ return i.index < len(i.collection.items) } func (i *StringIterator) Current() string { if i.index < len(i.collection.items) { return i.collection.items[i.index] } return "" } // Usage example func TraverseCollection(collection Collection) { iterator := collection.CreateIterator() for iterator.Next() { fmt.Println(iterator.Current()) } }
Summary
The Go language provides flexible support for the implementation of design patterns through its interface and composition features:
- Creational Patterns: Use the singleton pattern to control the global state, and the factory pattern to encapsulate object creation.
- Structural Patterns: Achieve the functional extension of decorators, proxies, and adapters through composition.
- Behavioral Patterns: Decouple and implement the interaction logic of observers, strategies, and commands with the help of interfaces.
In actual development, an appropriate pattern should be selected according to specific scenarios:
- For microservice architectures, the factory pattern and the adapter pattern are preferred.
- For event-driven systems, the observer pattern and the command pattern are recommended.
- For hierarchical data management, the composite pattern and the iterator pattern are the first choices.
The reasonable use of design patterns can improve the maintainability, extensibility, and reusability of the code. However, over-design should be avoided, and the focus should always be on solving practical problems.
Leapcell: The Best of Serverless Web Hosting
Finally, I would like to recommend a platform Leapcell that is most suitable for deploying Go services.
๐ Build with Your Favorite Language
Develop effortlessly in JavaScript, Python, Go, or Rust.
๐ Deploy Unlimited Projects for Free
Only pay for what you useโno requests, no charges.
โก Pay-as-You-Go, No Hidden Costs
No idle fees, just seamless scalability.
๐ Explore Our Documentation
๐น Follow us on Twitter: @LeapcellHQ