Fundamentals 36 min read

Overview of 10 Classic Software Design Patterns with Go Examples

The article presents ten classic software design patterns—Singleton, Factory, Observer, Decorator, Strategy, Adapter, Proxy, Command, Composite, and Iterator—explaining each pattern’s features, advantages, disadvantages, typical applications, and providing concrete Go code examples to illustrate their implementation.

Tencent Cloud Developer
Tencent Cloud Developer
Tencent Cloud Developer
Overview of 10 Classic Software Design Patterns with Go Examples

Software design patterns are a set of proven, reusable solutions to common design problems. This article, written by Tencent Cloud security engineer Wang Shunchi, introduces ten classic design patterns, discusses their characteristics, advantages, disadvantages, typical application scenarios, and provides concrete Go code examples for each.

1. Singleton Pattern

The Singleton pattern ensures that a class has only one instance and provides a global access point to it. It is useful when a single shared resource or global state is needed.

Features:

Only one instance object.

Instance must be created manually.

Provides a global access point.

Advantages:

Guarantees global uniqueness of resources or state.

Reduces resource consumption and improves efficiency.

Disadvantages:

Breaks modularity; multiple clients depend on the same instance.

Hard to test because the instance lives for the whole application lifetime.

Typical Use Cases:

Configuration manager.

Database connection pool.

Logger.

Hardware manager (e.g., printer).

Application state manager.

// Define a struct Singleton to hold the instance data
type singleton struct {
value string // any data the singleton holds
}
// Global variable to store the singleton instance
var instance *singleton
// getInstance returns the singleton instance
func getInstance() *singleton {
if instance == nil {
instance = &singleton{value: "unique instance"}
}
return instance
}
func main() {
s1 := getInstance()
fmt.Println(s1.value) // output: unique instance
s2 := getInstance()
if s1 == s2 {
fmt.Println("Both instances are the same") // output: Both instances are the same
}
}

2. Factory Pattern

The Factory pattern encapsulates object creation, allowing subclasses to decide which concrete class to instantiate. It improves code clarity and makes it easy to extend or replace product classes.

Features:

Encapsulation of object creation.

Extensibility via inheritance and polymorphism.

Abstraction: the factory method defines an interface, concrete factories implement it.

Advantages:

Separates creation from usage, increasing module independence.

Easy to add new product types without modifying existing code (Open/Closed Principle).

Disadvantages:

Each new product may require a new factory class, increasing class count.

Factory can become large and complex.

Typical Use Cases:

Database connection creation based on DB type.

GUI component creation for different platforms.

Payment gateway adapters.

Image processing handlers for various formats.

// Product interface
type Product interface {
operation()
}
// Concrete products
type ConcreteProductA struct{}
func (p *ConcreteProductA) operation() { fmt.Println("Operation of ConcreteProductA") }
type ConcreteProductB struct{}
func (p *ConcreteProductB) operation() { fmt.Println("Operation of ConcreteProductB") }
// Creator interface
type Creator interface {
factoryMethod() Product
}
// Concrete creators
type CreatorA struct{}
func (c *CreatorA) factoryMethod() Product { return &ConcreteProductA{} }
type CreatorB struct{}
func (c *CreatorB) factoryMethod() Product { return &ConcreteProductB{} }
func main() {
creatorA := &CreatorA{}
productA := creatorA.factoryMethod()
productA.operation()
creatorB := &CreatorB{}
productB := creatorB.factoryMethod()
productB.operation()
}

3. Observer Pattern

The Observer pattern defines a one‑to‑many dependency so that when the subject changes state, all its observers are automatically notified.

Features:

One‑to‑many relationship.

Loose coupling between subject and observers.

Dynamic addition/removal of observers.

Advantages:

Reduces coupling; subjects and observers are loosely connected.

Good extensibility; new observers can be added without affecting existing code.

Disadvantages:

Potential performance impact when many observers exist.

Complex dependency graphs can become hard to maintain.

Typical Use Cases:

Event listening systems in GUIs.

UI updates when data models change.

Message broadcasting in chat applications.

Stock price updates for subscribed investors.

Resource monitoring alerts.

// Observer interface
type Observer interface {
Update(string)
}
// Subject holds observers
type Subject struct {
observers []Observer
}
func (s *Subject) Attach(o Observer) { s.observers = append(s.observers, o) }
func (s *Subject) Notify(msg string) { for _, o := range s.observers { o.Update(msg) } }
// Concrete observer
type ConcreteObserverA struct { name string }
func (c *ConcreteObserverA) Update(msg string) { fmt.Printf("%s received: %s\n", c.name, msg) }
func main() {
subject := &Subject{}
observerA := &ConcreteObserverA{name: "Observer A"}
subject.Attach(observerA)
subject.Notify("State changed to State 1")
}

4. Decorator Pattern

The Decorator pattern adds responsibilities to objects dynamically without altering their structure, keeping the original interface intact.

Features:

Dynamic extension at runtime.

Transparency: decorators do not change the object's interface.

Flexibility: multiple decorators can be stacked.

Advantages:

Responsibilities can be added or removed dynamically.

Multiple decorators can be combined.

Components and decorators evolve independently.

Disadvantages:

Overuse can make the system complex and hard to understand.

Potential performance overhead due to deep decorator chains.

Typical Use Cases:

Logging.

Caching.

Access control.

Transaction handling.

Performance monitoring.

Resource management.

// Component interface
type Component interface { operation() }
// Concrete component
type ConcreteComponent struct{}
func (c *ConcreteComponent) operation() { fmt.Println("ConcreteComponent: performing basic operation") }
// Decorator base
type Decorator struct { component Component }
func (d *Decorator) operation() { if d.component != nil { d.component.operation() } }
// Concrete decorator A
type ConcreteDecoratorA struct { Decorator }
func (cda *ConcreteDecoratorA) operation() { cda.Decorator.operation(); fmt.Println("ConcreteDecoratorA: added additional responsibilities") }
func main() {
component := &ConcreteComponent{}
decoratorA := &ConcreteDecoratorA{Decorator{component}}
decoratorA.operation()
}

5. Strategy Pattern

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. It lets the algorithm vary independently from clients.

Features:

Encapsulates variations.

Uses polymorphism for algorithm swapping.

Runtime selection of strategy.

Advantages:

Algorithm changes are independent of client code.

Easy to add new algorithms without affecting existing clients.

Typical Use Cases:

Choosing different sorting algorithms.

Selecting payment methods.

Database connection strategies.

Game AI behavior selection.

// Strategy interface
type Strategy interface { algorithm() }
// Concrete strategies
type ConcreteStrategyA struct{}
func (c *ConcreteStrategyA) algorithm() { fmt.Println("Executing Algorithm A") }
type ConcreteStrategyB struct{}
func (c *ConcreteStrategyB) algorithm() { fmt.Println("Executing Algorithm B") }
// Context holds a strategy
type Context struct { strategy Strategy }
func (c *Context) executeStrategy() { c.strategy.algorithm() }
func main() {
ctx := &Context{}
ctx.strategy = &ConcreteStrategyA{}
ctx.executeStrategy()
ctx.strategy = &ConcreteStrategyB{}
ctx.executeStrategy()
}

6. Adapter Pattern

The Adapter pattern converts the interface of a class into another interface clients expect, allowing incompatible interfaces to work together.

Features:

Interface conversion.

Compatibility between otherwise incompatible classes.

Advantages:

Enables integration of disparate systems without modifying existing code.

Clients remain unchanged.

Typical Use Cases:

Integrating different subsystems.

Wrapping third‑party libraries with mismatched APIs.

Hardware device control adapters.

Legacy system migration.

// Target interface expected by client
type Target interface { request() }
// Existing class with a different interface
type Adaptee struct{}
func (a *Adaptee) specificRequest() { fmt.Println("Adaptee performs a specific request") }
// Adapter bridges Target and Adaptee
type Adapter struct { adaptee *Adaptee }
func (a *Adapter) request() { if a.adaptee != nil { a.adaptee.specificRequest() } }
func main() {
adaptee := &Adaptee{}
adapter := &Adapter{adaptee: adaptee}
var target Target = adapter
target.request()
}

7. Proxy Pattern

The Proxy pattern provides a surrogate or placeholder for another object to control access to it. It can add lazy initialization, access control, logging, etc.

Features:

Indirect access via a proxy.

Separation of responsibilities.

Lazy initialization.

Advantages:

Reduces coupling and adds controllability.

Can add security, caching, or other cross‑cutting concerns.

Extensible without modifying the real subject.

Typical Use Cases:

Access control.

Lazy loading of heavyweight objects.

Remote proxies for network resources.

Virtual proxies for resource‑intensive objects.

// Subject interface
type Subject interface { request() }
// Real subject
type RealSubject struct{}
func (r *RealSubject) request() { fmt.Println("Real Subject") }
// Proxy holds a reference to RealSubject
type Proxy struct { realSubject *RealSubject }
func (p *Proxy) request() { if p.realSubject == nil { p.realSubject = &RealSubject{} } p.realSubject.request() }
func main() {
var subject Subject = &Proxy{}
subject.request()
}

8. Command Pattern

The Command pattern encapsulates a request as an object, thereby allowing parameterization of clients with queues, requests, and operations.

Features:

Encapsulation of actions.

Supports undo/redo, logging, transaction.

Decouples invoker from receiver.

Advantages:

Reduces coupling between sender and receiver.

Enables flexible command management (queue, log, undo).

Easy to extend with new commands.

Typical Use Cases:

Transaction processing.

Undo/redo functionality.

Macro recording.

Logging user actions.

// Command interface
type Command interface { Execute() }
// Receiver
type Receiver struct{}
func (r *Receiver) Action() { fmt.Println("Receiver: Action") }
// Concrete command
type ConcreteCommand struct { receiver *Receiver }
func (c *ConcreteCommand) Execute() { c.receiver.Action() }
// Invoker
type Invoker struct { command Command }
func (i *Invoker) Invoke() { i.command.Execute() }
func main() {
r := &Receiver{}
cmd := &ConcreteCommand{receiver: r}
inv := &Invoker{command: cmd}
inv.Invoke()
}

9. Composite Pattern

The Composite pattern composes objects into tree structures to represent part‑whole hierarchies, allowing clients to treat individual objects and compositions uniformly.

Features:

Part‑whole hierarchy.

Uniform treatment of leaves and composites.

Advantages:

Simplifies client code.

Easy to extend hierarchical structures.

Typical Use Cases:

File system (files and directories).

Organization charts.

GUI component trees.

Distributed system resource aggregation.

// Component interface
type Component interface {
Operation()
Add(Component)
Remove(Component)
GetChild(int) Component
}
// Leaf node
type Leaf struct { name string }
func (l *Leaf) Operation() { fmt.Println("Leaf:", l.name) }
func (l *Leaf) Add(c Component) { fmt.Println("Cannot add to a leaf") }
func (l *Leaf) Remove(c Component) { fmt.Println("Cannot remove from a leaf") }
func (l *Leaf) GetChild(i int) Component { return nil }
// Composite node
type Composite struct { name string; Children []Component }
func (c *Composite) Operation() { fmt.Println("Composite:", c.name); for _, child := range c.Children { child.Operation() } }
func (c *Composite) Add(comp Component) { c.Children = append(c.Children, comp) }
func (c *Composite) Remove(comp Component) { for i, child := range c.Children { if child == comp { c.Children = append(c.Children[:i], c.Children[i+1:]...); break } } }
func (c *Composite) GetChild(i int) Component { if i < 0 || i >= len(c.Children) { return nil }; return c.Children[i] }
func main() {
leafA := &Leaf{name: "Leaf A"}
leafB := &Leaf{name: "Leaf B"}
root := &Composite{name: "Composite Root"}
root.Add(leafA)
root.Add(leafB)
root.Operation()
}

10. Iterator Pattern

The Iterator pattern provides a way to access the elements of an aggregate object sequentially without exposing its underlying representation.

Features:

Abstract traversal interface.

Supports multiple traversal strategies.

Decouples collection from traversal logic.

Typical Use Cases:

Iterating over collections.

Tree or graph traversal.

Database result set processing.

UI component iteration.

// Iterator interface
type Iterator interface { Next() bool; Current() interface{} }
// Concrete iterator
type ConcreteIterator struct { items []string; index int }
func (c *ConcreteIterator) Next() bool { if c.index < len(c.items) { c.index++; return true }; return false }
func (c *ConcreteIterator) Current() interface{} { if c.index > 0 && c.index <= len(c.items) { return c.items[c.index-1] }; return nil }
// Aggregate interface
type Aggregate interface { CreateIterator() Iterator }
// Concrete aggregate
type ConcreteAggregate struct { items []string }
func (a *ConcreteAggregate) CreateIterator() Iterator { return &ConcreteIterator{items: a.items, index: 0} }
func main() {
agg := &ConcreteAggregate{items: []string{"Item1", "Item2", "Item3"}}
it := agg.CreateIterator()
for it.Next() { fmt.Println(it.Current()) }
}

These ten patterns constitute a fundamental toolbox for designing robust, maintainable, and extensible software systems.

design patternssoftware architectureGosingletonStrategydecoratorFactoryObserver
Tencent Cloud Developer
Written by

Tencent Cloud Developer

Official Tencent Cloud community account that brings together developers, shares practical tech insights, and fosters an influential tech exchange community.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.