Design Patterns in Go with Examples: From A to Z
Design Patterns are time-tested architectural solutions that help Go developers create flexible, scalable, and maintainable code. Unlike classical OOP languages, Go offers its own unique approach to implementing patterns through interfaces, composition, and goroutines. In this article, we will break down the main design patterns in Go with specific code examples.
1. Creational Patterns
Creational patterns are responsible for object creation, making the system independent of how objects are created.
1.1. Singleton
The Singleton guarantees that a class has only one instance. In Go, this is often implemented using sync.Once for thread safety.
package main
import ( "fmt" "sync")
type Database struct { connection string}
var ( instance *Database once sync.Once)
func GetDatabaseInstance() *Database { once.Do(func() { instance = &Database{connection: "postgres://localhost:5432/mydb"} fmt.Println("Created a single database connection") }) return instance}
func main() { db1 := GetDatabaseInstance() db2 := GetDatabaseInstance() fmt.Println(db1 == db2) // true}1.2. Factory Method
The Factory delegates object creation to subclasses. In Go, this is done using interfaces.
package main
import "fmt"
// Product interfacetype Transport interface { Deliver() string}
// Concrete productstype Truck struct{}func (t Truck) Deliver() string { return "Delivery by land" }
type Ship struct{}func (s Ship) Deliver() string { return "Delivery by sea" }
// Factorytype LogisticsFactory interface { CreateTransport() Transport}
type RoadLogistics struct{}func (r RoadLogistics) CreateTransport() Transport { return Truck{} }
type SeaLogistics struct{}func (s SeaLogistics) CreateTransport() Transport { return Ship{} }
func main() { var factory LogisticsFactory = RoadLogistics{} transport := factory.CreateTransport() fmt.Println(transport.Deliver()) // Delivery by land}2. Structural Patterns
Structural patterns help organize classes and objects into larger structures.
2.1. Adapter
The Adapter allows objects with incompatible interfaces to work together. In Go, this is solved through a wrapper.
package main
import "fmt"
// Old interfacetype OldPrinter interface { PrintOld(data string)}
type LegacyPrinter struct{}func (l LegacyPrinter) PrintOld(data string) { fmt.Println("Legacy:", data)}
// New interfacetype NewPrinter interface { PrintNew(data string)}
// Adaptertype PrinterAdapter struct { OldPrinter OldPrinter}
func (a PrinterAdapter) PrintNew(data string) { a.OldPrinter.PrintOld(data)}
func main() { legacy := LegacyPrinter{} adapter := PrinterAdapter{OldPrinter: legacy} adapter.PrintNew("Hello, Go!") // Legacy: Hello, Go!}2.2. Composite
The Composite allows treating groups of objects as a single object. It is ideal for tree structures.
package main
import "fmt"
// Component interfacetype Graphic interface { Draw() string}
// Leaf elementtype Circle struct{}func (c Circle) Draw() string { return "Drawing a circle" }
// Containertype CompoundGraphic struct { children []Graphic}
func (cg *CompoundGraphic) Add(g Graphic) { cg.children = append(cg.children, g)}
func (cg *CompoundGraphic) Draw() string { result := "Compound figure:" for _, child := range cg.children { result += " - " + c