Welcome back, Gophers ❤
In the first part of this series, we mastered the art of creation learning how to spawn objects like a pro using Singletons, Factories, and Builders. But let’s be honest: creating objects is the easy part. The real challenge starts when those objects need to talk to each other without turning your codebase into a plate of spaghetti code.
Welcome to Part 2: Structural Patterns.
Think of these patterns as the architectural glue of your software. If Creational Patterns are about the bricks, Structural Patterns are about how you arrange those bricks to build a skyscraper that won’t collapse when the wind blows. We’re going to explore how to make your Go structs and interfaces work together in harmony, keeping your system flexible, efficient, and most importantly clean.
Ready to level up from a coder to an architect? Let’s dive into the structural magic of Go :)
2- Structural Patterns
Structural Patterns are design patterns that focus on how classes and objects are combined to create larger structures. They simplify the relationships between entities, making the system more flexible and easier to maintain.
Adapter Pattern:
We can imagine this pattern like a real electric adapter. When we plug an adapter into a socket, it converts high voltage to lower voltage.
Without adapters, we would need sockets for every voltage type in our walls. Thanks to adapters, we have one main source that we can convert to suit our needs.
Okay, I added an example for this method. First, let’s look at a diagram of the example, and after that, I will explain it.
Diagram:
First of all, we need to understand why we used the adapter pattern in our payment system. We implemented it to unify various payment methods under a single main interface because each payment system API has its own uniquely named functions.
- Define the Main Payment Interface: We start by defining a common interface with a single “Pay” method prototype.
- Implement Payment Structs and Functions: Next, we create structs for each payment method and implement their specific payment functions.
- Create Adapter Structs: Finally, we define adapter structs that wrap the existing payment structs. These adapter structs provide methods with the same name as the main payment interface’s method.
By using these adapter structs, we can call the specific payment functions through a unified interface, allowing us to work with different payment systems in a consistent manner.
Code:
package main
import "fmt"
//Payment interface with pay method
type Payment interface {
Pay(amount float32) string
}
//Paypal struct
type Paypal struct{}
//Paypal pay method
func (p *Paypal) payingWithPaypal(amount float32) string {
return fmt.Sprintf("Paying with paypal: %f", amount)
}
//Adapter for paypal
type adapterPaypal struct {
paypal *Paypal
}
//Pay method for adapter paypal
func (a *adapterPaypal) Pay(amount float32) string {
return a.paypal.payingWithPaypal(amount)
}
//Stripe struct
type Stripe struct{}
//Stripe pay method
func (s *Stripe) payingWithStripe(amount float32) string {
return fmt.Sprintf("Paying with stripe: %f", amount)
}
//Adapter for stripe
type adapterStripe struct {
stripe *Stripe
}
//Pay method for adapter stripe
func (a *adapterStripe) Pay(amount float32) string {
return a.stripe.payingWithStripe(amount)
}
func main() {
//Create paypal and stripe objects
paypal := &Paypal{}
stripe := &Stripe{}
//Create adapters
paypalAdapter := &adapterPaypal{paypal}
stripeAdapter := &adapterStripe{stripe}
//Pay with paypal and stripe
fmt.Println(paypalAdapter.Pay(100))
fmt.Println(stripeAdapter.Pay(200))
}
Bridge Pattern:
We use the Bridge method when our project has multiple interfaces and their related functions need to be combined. Instead of writing special functions for each combination, we utilize the Bridge pattern.
Okay, let’s look at an example diagram and code.
Diagram:
I want to explain the Bridge method using an example from the Adapter method. I’ve added some functions to make it suitable for the Bridge pattern. Now, we have a new, comprehensive interface for payment platforms and platform structures with their related order functions.
Previously, we linked all payment methods using the Adapter method, so when we use the general ‘Pay’ function, it selects the appropriate type for the operation.
Instead of writing specialized functions for each platform and payment method combination, we use the general ‘Pay’ function within the platform’s order functions.
Code:
package main
import "fmt"
// Payment interface with pay method
type Payment interface {
Pay(amount float32) string
}
// Paypal struct
type Paypal struct{}
// Paypal pay method
func (p *Paypal) payingWithPaypal(amount float32) string {
return fmt.Sprintf("Paying with paypal: %f", amount)
}
// Adapter for paypal
type adapterPaypal struct {
paypal *Paypal
}
// Pay method for adapter paypal
func (a *adapterPaypal) Pay(amount float32) string {
return a.paypal.payingWithPaypal(amount)
}
// Stripe struct
type Stripe struct{}
// Stripe pay method
func (s *Stripe) payingWithStripe(amount float32) string {
return fmt.Sprintf("Paying with stripe: %f", amount)
}
// Adapter for stripe
type adapterStripe struct {
stripe *Stripe
}
// Pay method for adapter stripe
func (a *adapterStripe) Pay(amount float32) string {
return a.stripe.payingWithStripe(amount)
}
// giveOrder interface
type giveOrder interface {
giveOrder(amount float32)
}
// WebSite struct
type WebSite struct {
Paying Payment
}
// giveOrder method for website
func (w *WebSite) giveOrder(amount float32) {
fmt.Printf("Order given: %f", amount)
w.Paying.Pay(amount)
}
//mobileApp struct
type MobileApp struct {
Paying Payment
}
// giveOrder method for mobileApp
func (m *MobileApp) giveOrder(amount float32) {
fmt.Printf("Order given: %f", amount)
m.Paying.Pay(amount)
}
func main() {
//Create paypal and stripe objects
paypal := &Paypal{}
stripe := &Stripe{}
//Create adapters
paypalAdapter := &adapterPaypal{paypal}
stripeAdapter := &adapterStripe{stripe}
//Create website and mobileApp objects
website := &WebSite{Paying: paypalAdapter}
mobileapp := &MobileApp{Paying: stripeAdapter}
//Give order
website.giveOrder(100)
mobileapp.giveOrder(200)
}
Composite Pattern:
We use the Composite Pattern for projects that involve tree-like structures. These structures can include elements that contain sub-elements or elements at the same level.
With this pattern, we can execute specific functions for each element.
Now, let’s take a look at an example diagram and code.
Diagram:
After creating all the objects and adding them, we use the top element’s “ShowInfo” function. Thanks to the “ShowInfo” function, we can execute the appropriate function for each element by calling the “ShowInfo” function of other types or by using it recursively.
Code:
package main
import "fmt"
// Component interface is the base interface for all components
type Component interface {
// ShowInfo method is used to show the information of the component
ShowInfo() string
}
// Employee struct is a leaf component
type Employee struct {
name string
}
// ShowInfo method is used to show the information of the employee
func (e Employee) ShowInfo() string {
return "\t Employee: " + e.name + "\n"
}
// Team struct is a composite component that contains employees and other teams
type Team struct {
name string
Components []Component
}
// ShowInfo method is used to show the information of the team
func (t Team) ShowInfo() string {
result := "Team: " + t.name + "\n"
for _, comp := range t.Components {
result += "\t" + comp.ShowInfo()
}
return result
}
// AddComponent method is used to add a component to the team
func (t *Team) AddComponent(c Component) {
t.Components = append(t.Components, c)
}
// Department struct is a composite component that contains teams and other departments
type Department struct {
name string
Components []Component
}
// ShowInfo method is used to show the information of the department
func (d Department) ShowInfo() string {
result := "Department: " + d.name + "\n"
for _, comp := range d.Components {
result += "\t" + comp.ShowInfo()
}
return result
}
// AddComponent method is used to add a component to the department
func (d *Department) AddComponent(c Component) {
d.Components = append(d.Components, c)
}
func main() {
// Diagram
/*
Department A
/ \
Deparment B Team A
/ \ / \
Team B Team C Team D Em. A
/ \ / \ / \
Em.B Em.C Em.D Em.E Em.F Em.G
*/
// Create Employees
employeeA := Employee{name: "A"}
employeeB := Employee{name: "B"}
employeeC := Employee{name: "C"}
employeeD := Employee{name: "D"}
employeeE := Employee{name: "E"}
employeeF := Employee{name: "F"}
employeeG := Employee{name: "G"}
// Create Teams
teamA := Team{name: "A"}
teamB := Team{name: "B"}
teamC := Team{name: "C"}
teamD := Team{name: "D"}
// Create Departments
departmentA := Department{name: "A"}
departmentB := Department{name: "B"}
// Add Employees to Teams
// Team D
teamD.AddComponent(employeeG)
teamD.AddComponent(employeeF)
// Team A
teamA.AddComponent(employeeA)
// Team B
teamB.AddComponent(employeeB)
teamB.AddComponent(employeeC)
// Team C
teamC.AddComponent(employeeD)
teamC.AddComponent(employeeE)
// Add Sub Teams to Teams
//Team A
teamA.AddComponent(teamD)
// Add Teams to Departments
// Department B
departmentB.AddComponent(teamB)
departmentB.AddComponent(teamC)
// Department A
departmentA.AddComponent(teamA)
// Add Sub Departments to Departments
// Department A
departmentA.AddComponent(departmentB)
// Show Info
fmt.Println(departmentA.ShowInfo())
}
Facade Pattern:
Architecture is often about hiding a messy truth behind a beautiful exterior. In Go, we use the Facade pattern to provide a clean and simple entry point to a complex set of classes. It acts as the master controller that shields users from internal systemic chaos.
Diagram:
By maintaining references to the TV, SoundSystem, and Lights, the facade manages their full lifecycle and ensures operations happen in the correct order.
The diagram illustrates a clean hierarchy where the facade wraps complex subsystems into a single interface. The user calls one simple command, triggering an orchestrated sequence across multiple objects that otherwise operate independently. This design keeps your main logic decoupled from low-level details, making the entire structure significantly more resilient to change.
Code:
package main
import fmt
// Subsystem 1: The Visuals
type TV struct{}
func (t *TV) On() {
fmt.Println(`TV is now powering on...`)
}
func (t *TV) Off() {
fmt.Println(`TV is shutting down.`)
}
// Subsystem 2: The Audio
type SoundSystem struct{}
func (s *SoundSystem) SetVolume(level int) {
fmt.Printf(`Adjusting speakers to volume level: %d\n`, level)
}
func (s *SoundSystem) Off() {
fmt.Println(`Surround sound system turned off.`)
}
// Subsystem 3: The Atmosphere
type Lights struct{}
func (l *Lights) Dim(percent int) {
fmt.Printf(`Dimming house lights to %d percent.\n`, percent)
}
func (l *Lights) Reset() {
fmt.Println(`Lights returned to full brightness.`)
}
// The Facade: The master controller for the theater
type HomeTheaterFacade struct {
tv *TV
audio *SoundSystem
lights *Lights
}
// NewHomeTheaterFacade initializes the complex dependencies
func NewHomeTheaterFacade() *HomeTheaterFacade {
return &HomeTheaterFacade{
tv: &TV{},
audio: &SoundSystem{},
lights: &Lights{},
}
}
// WatchMovie provides the simplified entry point
func (h *HomeTheaterFacade) WatchMovie() {
fmt.Println(`Preparing the theater for your movie...`)
h.lights.Dim(20)
h.tv.On()
h.audio.SetVolume(15)
fmt.Println(`Enjoy the show!`)
}
// EndMovie handles the cleanup in one call
func (h *HomeTheaterFacade) EndMovie() {
fmt.Println(`Cleaning up after the movie...`)
h.tv.Off()
h.audio.Off()
h.lights.Reset()
}
func main() {
// The client only interacts with the simple facade interface
theater := NewHomeTheaterFacade()
// Start the complex sequence with one click
theater.WatchMovie()
fmt.Println(`--- Time passes ---`)
// Shut everything down with one click
theater.EndMovie()
}
Proxy Pattern:
Now that we’ve simplified the interface with Facade, let’s see how we can control access to those objects using Proxy.
Proxy pattern acts as a sophisticated gatekeeper for your objects. While the Facade pattern simplifies an entire subsystem, the Proxy pattern focuses on a single object, acting as a substitute or placeholder to control access to it. It can handle lazy initialization, perform access control, or add logging without the client ever knowing the difference
Diagram:
To understand how the Proxy sits between your user and your logic, let us look at a network server implementation. The following diagram visualizes how the Proxy implements the same interface as the real service, allowing it to disguise itself perfectly while managing the request lifecycle.
The true strength of the Proxy stems from its ability to act as a seamless intermediary where both the proxy and the actual service implement the same interface to remain perfectly interchangeable from the perspective of the client.
By taking full responsibility for the lifecycle of the service object, it can defer heavy initialization until the exact moment it is needed, effectively saving system resources.
Most importantly, it serves as a sophisticated gatekeeper that can validate credentials, enforce rate limits, or return cached results before the real application logic is even triggered, keeping your core system protected and highly efficient.
Code:
package main
import "fmt"
// Server defines the common interface for both proxy and real object
type Server interface {
HandleRequest(url, method string) (int, string)
}
// RealApplication provides the primary business logic
type RealApplication struct{}
func (r *RealApplication) HandleRequest(url, method string) (int, string) {
if url == "/app/status" && method == "GET" {
return 200, "OK: Application is healthy"
}
return 404, "Not Found"
}
// NginxProxy acts as a placeholder to control access
type NginxProxy struct {
application *RealApplication
maxAllowedRequest int
rateLimiter map[string]int
}
// NewNginxProxy manages the lifecycle of the RealApplication
func NewNginxProxy() *NginxProxy {
return &NginxProxy{
application: &RealApplication{},
maxAllowedRequest: 2,
rateLimiter: make(map[string]int),
}
}
// HandleRequest performs access control before delegating to the real service
func (n *NginxProxy) HandleRequest(url, method string) (int, string) {
allowed := n.checkRateLimit(url)
if !allowed {
return 403, "Not Allowed: Rate limit exceeded"
}
// Logging behavior added by the proxy
fmt.Printf("Proxy: Forwarding %s request to %s\n", method, url)
// Delegation to the real service
return n.application.HandleRequest(url, method)
}
func (n *NginxProxy) checkRateLimit(url string) bool {
if n.rateLimiter[url] >= n.maxAllowedRequest {
return false
}
n.rateLimiter[url]++
return true
}
func main() {
// The Client works with the Proxy via the Server interface
var server Server = NewHomeProxy()
appUrl := "/app/status"
// Request 1: Passes through
code, body := server.HandleRequest(appUrl, "GET")
fmt.Printf("Status: %d | Body: %s\n", code, body)
// Request 2: Passes through
code, body = server.HandleRequest(appUrl, "GET")
fmt.Printf("Status: %d | Body: %s\n", code, body)
// Request 3: Blocked by the Proxy (Rate Limit)
code, body = server.HandleRequest(appUrl, "GET")
fmt.Printf("Status: %d | Body: %s\n", code, body)
}
// Helper to match diagram name
func NewHomeProxy() Server {
return NewNginxProxy()
}
Decorator Pattern:
While inheritance feels like a permanent tattoo on your class hierarchy, decoration is more like a custom outfit you change based on the weather. In the world of Go, we often reach a point where creating a new subclass for every possible feature combination leads to a combinatorial explosion of types. The Decorator pattern solves this by letting you wrap objects inside other objects, adding superpowers to them at runtime without ever touching the original code.
Think of it as the programming equivalent of a Matryoshka doll. You start with a simple base, and then you keep wrapping it in layers of functionality. Each layer knows how to do its own small job and then delegates the rest to the object inside it.
Diagram:
The genius of this pattern is found in its recursive nature where both the foundation and the enhancements speak the same language through a shared interface. By treating the core component and its wrappers as equals, we can stack behaviors infinitely like an onion, allowing each layer to perform its specific duty before handing the task deeper into the stack.
This approach keeps your primary business logic isolated from the endless combinations of optional features, giving your system the freedom to grow and adapt without the need for a massive, hard-to-maintain hierarchy .
Code:
package main
import fmt
// Beverage defines the common ground for all coffee types
type Beverage interface {
GetDescription() string
GetPrice() int
}
// BasicCoffee is the essential core of our system
type BasicCoffee struct{}
func (c *BasicCoffee) GetDescription() string {
return "Basic Coffee"
}
func (c *BasicCoffee) GetPrice() int {
return 10
}
// MilkDecorator enhances the beverage with milk logic
type MilkDecorator struct {
beverage Beverage
}
func (m *MilkDecorator) GetDescription() string {
return m.beverage.GetDescription() + ", Milk"
}
func (m *MilkDecorator) GetPrice() int {
return m.beverage.GetPrice() + 5
}
// SugarDecorator adds a sweet layer to the drink
type SugarDecorator struct {
beverage Beverage
}
func (s *SugarDecorator) GetDescription() string {
return s.beverage.GetDescription() + ", Sugar"
}
func (s *SugarDecorator) GetPrice() int {
return s.beverage.GetPrice() + 2
}
func main() {
// We start with a plain order
order := Beverage(&BasicCoffee{})
fmt.Printf("Initial Order: %s | Total: %d\n", order.GetDescription(), order.GetPrice())
// We dynamically wrap the order in milk
order = &MilkDecorator{beverage: order}
fmt.Printf("Updated Order: %s | Total: %d\n", order.GetDescription(), order.GetPrice())
// Finally, we add sugar on top of the milk-wrapped coffee
order = &SugarDecorator{beverage: order}
fmt.Printf("Final Order: %s | Total: %d\n", order.GetDescription(), order.GetPrice())
}
Flyweight Pattern:
Now we arrive at our final destination: the Flyweight pattern. This is not just a performance trick; it is an act of respect for limited resources. Imagine rendering a forest with millions of trees in a game. If every tree carries its own heavy textures and models, your RAM will suffocate.
Flyweight asks a simple question: Does every object really need its own copy of everything? By sharing common traits like colors or textures across millions of instances, we keep the system light and efficient
Diagram:
At the heart of the Flyweight lies the division of state. The TreeType represents the intrinsic state heavy data like names and colors that never change. The Tree represents the extrinsic state unique data like coordinates and a pointer to the shared type. The TreeFactory acts as a gatekeeper, ensuring that if a type already exists in memory, it is reused rather than duplicated.
This allows your system to handle massive amounts of objects while maintaining a minimal memory footprint.
Code:
package main
import "fmt"
// TreeType stores shared, unchanging data (Intrinsic)
type TreeType struct {
name string
color string
}
func (t *TreeType) Draw(x, y int) {
fmt.Printf("Drawing %s tree in %s at %d:%d\n", t.name, t.color, x, y)
}
// TreeFactory manages and reuses shared objects
type TreeFactory struct {
treeTypes map[string]*TreeType
}
func (f *TreeFactory) GetTreeType(name, color string) *TreeType {
if f.treeTypes == nil {
f.treeTypes = make(map[string]*TreeType)
}
if _, exists := f.treeTypes[name]; !exists {
f.treeTypes[name] = &TreeType{name: name, color: color}
fmt.Println("Shared type created: "+ name)
}
return f.treeTypes[name]
}
// Tree stores unique, contextual data (Extrinsic)
type Tree struct {
x, y int
typePtr *TreeType
}
func (t *Tree) Draw() {
t.typePtr.Draw(t.x, t.y)
}
func main() {
factory := &TreeFactory{}
pineType := factory.GetTreeType("Pine", "Green")
forest := []Tree{
{x: 1, y: 5, typePtr: pineType},
{x: 10, y: 20, typePtr: pineType},
}
for _, tree := range forest {
tree.Draw()
}
}
We’ve covered a lot of ground today. To help you choose the right tool for your architectural needs, I’ve prepared a quick summary table. Think of this as your cheat sheet for structural patterns:
And there you have it. We’ve just toured the structural wonders of the Go world. From the Adapter to the Flyweight, you now have the tools to compose complex systems that remain elegant and easy to manage.
Remember: A good developer writes code that works; a great developer builds structures that last.
But wait… our journey isn’t over yet. We know how to create objects, and we know how to organize them. But how do we handle the complex communication and ‘social lives’ of these objects? How do they react when things change?
In the third and final part of this series, we will dive into Behavioral Patterns. We’ll be talking about Strategy, Observer, State, and more. It’s where your code truly comes to life.
Stay tuned, keep coding, and don’t let your pointers dangle.
See you in Part-3 🚀



















Top comments (0)