Design patterns are reusable solutions to common problems in software design.
They provide a shared language for developers and encourage best practices in system architecture.
In this article, we’ll explore some of the most widely used design patterns in Go, grouped into three categories: creational, structural, and behavioral.

๐ง Creational Patterns
๐ Singleton
Ensures a class has only one instance and provides a global point of access to it.
โWhen discussing which pattern to drop, we found that we still love them all. (Not really โ I’m in favor of dropping Singleton. Its use is almost always a design smell.)โ
โ Erich Gamma, Design Patterns: Elements of Reusable Object-Oriented SoftwareWhile Singleton often gets a bad reputation, there are still valid use cases in Go:
- โ You only want one component in the system (e.g., database repository, object factory)
- โณ The object is expensive to construct, so you instantiate it only once
- ๐ซ You want to prevent the creation of additional instances
- ๐ค You want lazy instantiation (e.g. load config or connect to DB only when needed)
Go makes this easy and thread-safe with
sync.Once
. To stay testable and modular, follow the Dependency Inversion Principle (DIP) โ depend on interfaces, not concrete types.Hint:
Singleton quite often breaks the Dependency Inversion Principle!
๐งโ๐ป Example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
package singleton import ( "sync" ) var ( instance *singleton once sync.Once ) type singleton struct{} func GetInstance() *singleton { once.Do(func() { instance = &singleton{} }) return instance }
๐งช Usage
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
package main import ( "fmt" "singleton" ) func main() { a := singleton.GetInstance() b := singleton.GetInstance() fmt.Println(a.Value) // Output: I am the only one // Confirm both variables point to the same instance fmt.Println(a == b) // Output: true }
๐ญ Factory
Creates objects without specifying the exact class.
A factory helps simplify object creation when:
- ๐ Object creation logic becomes too convoluted
- ๐งฑ A struct has too many fields that need to be correctly initialized
- ๐ก You want to delegate creation logic away from the calling code
There are two flavors of factories in Go:
- ๐ง Factory function (also called a
constructor
): a helper function to initialize struct instances - ๐๏ธ Factory struct: a dedicated struct responsible for managing object creation
Unlike the Builder pattern, which is piecewise, the Factory creates the object wholesale โ usually in one go.
๐งโ๐ป Example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
package factory type Shape interface { Draw() string } type Circle struct{} func (c Circle) Draw() string { return "Drawing Circle" } type Square struct{} func (s Square) Draw() string { return "Drawing Square" } func GetShape(shapeType string) Shape { switch shapeType { case "circle": return Circle{} case "square": return Square{} default: return nil } }
๐งช Usage
1 2 3 4 5 6 7 8 9 10 11 12 13 14
package main import ( "fmt" "factory" ) func main() { circle := factory.GetShape("circle") square := factory.GetShape("square") fmt.Println(circle.Draw()) // Output: Drawing Circle fmt.Println(square.Draw()) // Output: Drawing Square }
๐งฑ Builder
Separates the construction of a complex object from its representation.
Not all objects are created equal:
- โ Some are simple and can be created with a single constructor call
- โ ๏ธ Others require a lot of ceremony to set up
- ๐งฉ Factory functions with 10+ parameters become hard to use and maintain
When you want more flexibility and readability, use the Builder pattern.
- ๐ ๏ธ A Builder is a separate component used to construct an object step-by-step
- ๐ It exposes a fluent API โ each method returns the receiver (
*Builder
) to enable chaining - ๐ง In advanced designs, different builders can operate on different facets of the same object
๐งโ๐ป Example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
package builder type Car struct { Engine string Wheels int Color string } type CarBuilder struct { car Car } func (b *CarBuilder) SetEngine(engine string) *CarBuilder { b.car.Engine = engine return b } func (b *CarBuilder) SetWheels(wheels int) *CarBuilder { b.car.Wheels = wheels return b } func (b *CarBuilder) SetColor(color string) *CarBuilder { b.car.Color = color return b } func (b *CarBuilder) Build() Car { return b.car }
๐งช Usage
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
package main import ( "fmt" "builder" ) func main() { car := builder.CarBuilder{}. SetEngine("V8"). SetWheels(4). SetColor("Red"). Build() fmt.Printf("%+v\n", car) // Output: {Engine:V8 Wheels:4 Color:Red} }
๐งฉ Structural Patterns
1.๐ Adapter
Allows incompatible interfaces to work together.
An Adapter is a design construct that adapts an existing interface X to conform to the required interface Y. It acts as a translator or bridge between two systems that otherwise couldnโt work together.
๐งญ To implement an adapter in Go:
- ๐ Determine the **API you have** (e.g. `Adaptee`)
- ๐ฏ Define the **API you need** (e.g. `Target`)
- ๐งฉ Create an adapter struct that **aggregates** the adaptee (usually via a pointer)
- โก Optimize when needed โ adapters may introduce intermediate representations, so use **caching** or other performance strategies as required
This is especially useful when integrating legacy code or 3rd-party libraries into a new system with different interfaces.
๐งโ๐ป Example:
|
|
๐งช Usage
|
|
๐ Decorator
Adds behavior to objects dynamically by embedding and extending existing functionality.
The Decorator pattern is used when you want to:
- โ Augment an object with additional behavior
- ๐ซ Avoid modifying existing code (โ Open/Closed Principle โ OCP)
- ๐งผ Keep new functionality separate and modular (โ Single Responsibility Principle โ SRP)
- ๐ Retain the ability to interact with existing interfaces
The solution is to embed the decorated object and override or extend its behavior. This lets you build stackable, reusable enhancements without altering the base struct.
๐งโ๐ป Example: wrapping a basic
Coffee
with aMilkDecorator
:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
package decorator type Coffee interface { Cost() float64 } type SimpleCoffee struct{} func (s SimpleCoffee) Cost() float64 { return 2.0 } type MilkDecorator struct { Coffee } func (m MilkDecorator) Cost() float64 { return m.Coffee.Cost() + 0.5 }
๐งช Usage
1 2 3 4 5 6 7 8 9 10 11 12 13 14
package main import ( "fmt" "decorator" ) func main() { var coffee decorator.Coffee = decorator.SimpleCoffee{} fmt.Println("Base cost:", coffee.Cost()) // Output: 2.0 coffeeWithMilk := decorator.MilkDecorator{Coffee: coffee} fmt.Println("With milk:", coffeeWithMilk.Cost()) // Output: 2.5 }
๐ก Proxy
Provides a surrogate or placeholder.
๐งโ๐ป Example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
package proxy type Image interface { Display() string } type RealImage struct { filename string } func (r RealImage) Display() string { return "Displaying " + r.filename } type ProxyImage struct { realImage *RealImage filename string } func (p *ProxyImage) Display() string { if p.realImage == nil { p.realImage = &RealImage{filename: p.filename} } return p.realImage.Display() }
๐งช Usage
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
package main import ( "fmt" "proxy" ) func main() { img := &proxy.ProxyImage{filename: "cat.png"} // The real image is not loaded yet fmt.Println(img.Display()) // Output: Displaying cat.png // The real image is reused without reloading fmt.Println(img.Display()) // Output: Displaying cat.png }
๐ณ Composite
Composes objects into tree structures. Composes objects into tree structures and lets you treat individual and composite objects uniformly.
The Composite pattern is ideal when some components are single objects (like files), and others are containers of other components (like folders). Both should support a common interface so clients donโt need to differentiate between them.
๐งญ To implement a composite in Go:
๐งฑ Define a common interface that all components implement.
๐ฟ Implement Leaf objects (e.g. File, Button, TextField).
๐งบ Implement Composite objects (e.g. Folder, Panel) that aggregate children and delegate behavior to them.
๐ Add iteration if you need to traverse or walk the tree (e.g. using the Iterator pattern).
This pattern shines when building hierarchical or nested structures such as UI components, file systems, or organization charts.
๐งโ๐ป Example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
package composite type Component interface { Operation() string } type Leaf struct { name string } func (l Leaf) Operation() string { return l.name } type Composite struct { children []Component } func (c *Composite) Add(child Component) { c.children = append(c.children, child) } func (c *Composite) Operation() string { result := "" for _, child := range c.children { result += child.Operation() + " " } return result }
๐ฆ Example usage:
1 2 3 4 5 6 7 8 9 10
func main() { file1 := Leaf{name: "FileA.txt"} file2 := Leaf{name: "FileB.txt"} folder := &Composite{} folder.Add(file1) folder.Add(file2) fmt.Println(folder.Operation()) // Output: FileA.txt FileB.txt }
โ When to use Composite:
- You want to treat individual and group objects the same way
- You have recursive or nested structures
- You want to delegate behavior to child components
๐ Bonus: Pair with the Iterator pattern to walk tree structures cleanly without exposing their internal representation.
๐ง Behavioral Patterns
๐งฎ Strategy
Defines a family of algorithms.
Encapsulates a family of algorithms and allows them to be selected and swapped at runtime.
The Strategy pattern is used when you want to:
- ๐ง Separate an algorithm into its skeleton and implementation steps
- ๐งฉ Decompose behavior into high-level workflows and low-level operations
- ๐ Swap logic dynamically without changing the calling code
- โ Adhere to the Open/Closed Principle (OCP) โ new strategies without changing the high-level logic
The solution is to define a high-level algorithm that delegates part of its logic to an injected strategy. This strategy follows a shared interface, so any implementation can be plugged in without breaking the algorithm. ๐ต Analogy: making a hot beverage
Many real-world algorithms follow this structure. Take making tea as an example:
Skeleton algorithm: Boil water โ Pour into cup โ Add ingredient Concrete implementation: Add tea bag, coffee grounds, or cocoa powder
The high-level process is reusable, and the final step is delegated to a drink-specific strategy. This is exactly how Strategy works.
๐งโ๐ป Example: Choosing an operation strategy
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
package strategy type Strategy interface { Execute(a, b int) int } type Add struct{} func (Add) Execute(a, b int) int { return a + b } type Multiply struct{} func (Multiply) Execute(a, b int) int { return a * b } type Context struct { strategy Strategy } func (c *Context) SetStrategy(s Strategy) { c.strategy = s } func (c Context) ExecuteStrategy(a, b int) int { return c.strategy.Execute(a, b) }
๐งช Usage
1 2 3 4 5 6 7
ctx := strategy.Context{} ctx.SetStrategy(strategy.Add{}) fmt.Println(ctx.ExecuteStrategy(3, 4)) // Output: 7 ctx.SetStrategy(strategy.Multiply{}) fmt.Println(ctx.ExecuteStrategy(3, 4)) // Output: 12
By:
- Defining a common interface (Strategy)
- Creating multiple concrete strategies (Add, Multiply)
- Supporting runtime injection into a reusable context (Context)
You separate the structure of the algorithm from its implementation. Just like boiling water and pouring it into a cup โ what happens next depends on the drink you’re making.
This makes your code modular, extensible, and easy to adapt to new behaviors without touching your existing flow.
๐ Observer
Wants to listen to events and be notified when something happens.
The Observer pattern is used when you want to:
- ๐ฃ Be informed when a particular object changes state, does something, or reacts to an external event
- ๐ Let other objects (observers) subscribe to and react to those changes
- ๐ Decouple the source of truth from those reacting to it
- โ Support dynamic subscription and unsubscription
The solution is to have two participants:
- ๐ข Observable: emits events and holds a list of observers
- ๐ก Observer: subscribes and reacts to events
When the observable changes, it notifies all observers โ sending event data (commonly as interface{} in Go) to each subscriber. This is an intrusive approach since the observable must provide explicit subscription management.
๐งโ๐ป Example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
package observer type Observer interface { Update(string) } type Subject interface { Attach(Observer) Notify() } type ConcreteSubject struct { observers []Observer state string } func (s *ConcreteSubject) Attach(o Observer) { s.observers = append(s.observers, o) } func (s *ConcreteSubject) SetState(state string) { s.state = state s.Notify() } func (s *ConcreteSubject) Notify() { for _, o := range s.observers { o.Update(s.state) } } type ConcreteObserver struct { id string } func (o ConcreteObserver) Update(state string) { println("Observer", o.id, "received new state:", state) }
๐งช Usage
1 2 3 4 5 6 7 8 9 10 11 12
subject := &observer.ConcreteSubject{} observer1 := observer.ConcreteObserver{id: "A"} observer2 := observer.ConcreteObserver{id: "B"} subject.Attach(observer1) subject.Attach(observer2) subject.SetState("๐ Launching") // Output: // Observer A received new state: ๐ Launching // Observer B received new state: ๐ Launching
With Observer, you give objects the ability to react automatically to changes elsewhere, without tightly coupling them together. This pattern is especially helpful for:
- UIs reacting to data changes
- Logging and monitoring
- Event-based systems
Hint:
This approach is intrusive โ the observable must explicitly support subscriptions and notify logic.
๐ State
Allows an object to alter its behavior when its internal state changes โ effectively changing its class at runtime.
The State pattern is used when you want to:
- ๐ Let an object change behavior dynamically based on its current state
- ๐ฒ Model real-world systems where actions depend on state
- ๐ง Manage complex state logic in a modular, maintainable way
The solution is to encapsulate each state in its own type and let the context object delegate behavior to the current state. When the state changes, so does the object’s behavior โ without conditionals scattered throughout the code.
These transitions are triggered by events (e.g. dialing, picking up, hanging up), and actions vary depending on the state. This is a perfect fit for a state machine โ a formal model that defines:
- ๐ฅ Entry/exit actions for each state
- ๐ Transitions between states, often triggered by events
- โ Guards that control whether a transition is allowed
- โ๏ธ A default behavior if no transition is found
When systems grow in complexity, it pays to define states and transitions explicitly to keep logic clean and modular.
๐งโ๐ป Example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
package state type State interface { Handle() string } type Context struct { state State } func (c *Context) SetState(s State) { c.state = s } func (c Context) Request() string { return c.state.Handle() } type OnState struct{} func (OnState) Handle() string { return "State is ON" } type OffState struct{} func (OffState) Handle() string { return "State is OFF" }
๐งช Usage
1 2 3 4 5 6 7
ctx := state.Context{} ctx.SetState(state.OnState{}) fmt.Println(ctx.Request()) // Output: State is ON ctx.SetState(state.OffState{}) fmt.Println(ctx.Request()) // Output: State is OFF
With the State pattern:
- You encapsulate each state and its logic in a separate type
- The object transitions explicitly in response to triggers
- Behavior is cleanly modular, without long chains of if or switch
Whether you’re modeling a telephone, a TCP connection, or a video player, state machines help you handle transitions with clarity, flexibility, and control.
โ Conclusion
Design patterns are powerful tools in every Go developerโs toolkit. While Go encourages simplicity, these patterns still applyโespecially in large-scale systems or when writing reusable libraries. Using patterns like Singleton, Adapter, and Strategy can lead to cleaner, more testable, and maintainable code.
Happy Go coding! ๐น
๐ Follow me on norbix.dev for more insights on Go, system design, and engineering wisdom.