This series of articles looks at various patterns and how they apply to Go, continuing with with the GoF structural patterns.
Verdicts: Link to heading
- Idiomatic - Considered a best practice in Go.
- Code Smell - Not wrong, but if you are using it, make sure you understand what you are doing.
- Anti-Pattern - Avoid unless you are an expert and know it is okay to break this rule.
Bridge Pattern Link to heading
Problem Space Link to heading
The Bridge pattern decouples an abstraction from its implementation so the two can vary independently. In classic OOP, this involves separate inheritance hierarchies connected by a “bridge.” In Go, it’s simpler: hold an interface as a field, and swap implementations as needed.
Practical Example 1 Link to heading
internal/notification/notification.go
package notification
type Sender interface {
Send(to, message string) error
}
type Notifier struct {
sender Sender
}
func NewNotifier(s Sender) *Notifier {
return &Notifier{sender: s}
}
func (n *Notifier) Alert(to, message string) error {
return n.sender.Send(to, "[ALERT] "+message)
}
func (n *Notifier) Remind(to, message string) error {
return n.sender.Send(to, "[REMINDER] "+message)
}
internal/notification/senders.go
package notification
import "fmt"
type EmailSender struct {
smtpHost string
}
func NewEmailSender(host string) *EmailSender {
return &EmailSender{smtpHost: host}
}
func (e *EmailSender) Send(to, message string) error {
fmt.Printf("Email to %s: %s\n", to, message)
return nil
}
type SMSSender struct {
apiKey string
}
func NewSMSSender(apiKey string) *SMSSender {
return &SMSSender{apiKey: apiKey}
}
func (s *SMSSender) Send(to, message string) error {
fmt.Printf("SMS to %s: %s\n", to, message)
return nil
}
cmd/cli/main.go
package main
import "myapp/internal/notification"
func main() {
// Same Notifier abstraction, different implementations
emailNotifier := notification.NewNotifier(
notification.NewEmailSender("smtp.example.com"),
)
emailNotifier.Alert("user@example.com", "server down")
smsNotifier := notification.NewNotifier(
notification.NewSMSSender("api-key-123"),
)
smsNotifier.Remind("555-1234", "meeting in 10 minutes")
}
Practical Example 2 Link to heading
internal/report/report.go
package report
import "io"
type Report struct {
title string
content string
writer io.Writer
}
func New(title, content string, w io.Writer) *Report {
return &Report{title: title, content: content, writer: w}
}
func (r *Report) Render() error {
_, err := io.WriteString(r.writer, r.title+"\n\n"+r.content)
return err
}
cmd/cli/main.go
package main
import (
"bytes"
"myapp/internal/report"
"os"
)
func main() {
// Same Report, different output destinations
consoleReport := report.New("Status", "All systems operational", os.Stdout)
consoleReport.Render()
var buf bytes.Buffer
bufferedReport := report.New("Status", "All systems operational", &buf)
bufferedReport.Render()
// buf now contains the report
}
Considerations Link to heading
Example 1: The abstraction (Notifier) holds an interface (Sender). You can add new notification types (Alert, Remind, etc.) without touching senders, and add new senders without touching the notifier.
Example 2: Using io.Writer as the bridge. The Report doesn’t care where output goes — stdout, file, buffer, network. This is Bridge at its simplest.
In Go, Bridge is just composition with interfaces. If you’re holding an interface as a struct field and injecting implementations, you’re using Bridge. It’s so idiomatic that most Go developers use it without knowing the pattern name.
Verdict Link to heading
- Example 1: Idiomatic
- Example 2: Idiomatic
Adapter Pattern Link to heading
Problem Space Link to heading
The Adapter pattern converts one interface into another that clients expect. It lets types work together that couldn’t otherwise because of incompatible interfaces. In Go, this is extremely common due to implicit interface satisfaction, you wrap a type to make it satisfy an interface it doesn’t natively implement.
Practical Example 1 Link to heading
cmd/server/main.go
package main
import (
"fmt"
"net/http"
)
func greet(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "hello")
}
func main() {
// http.HandlerFunc adapts a function to satisfy http.Handler
http.Handle("/", http.HandlerFunc(greet))
http.ListenAndServe(":8080", nil)
}
Practical Example 2 Link to heading
internal/storage/storage.go
package storage
import (
"bytes"
"io"
)
// Third-party client that doesn't implement io.Reader
type BlobClient struct {
// ...
}
func (b *BlobClient) Fetch(key string) ([]byte, error) {
// fetch from remote storage
return []byte("data"), nil
}
// Adapter wraps BlobClient to satisfy io.Reader
type BlobReader struct {
client *BlobClient
key string
// Embed or hold a reader.
// Since we fetch lazily, we hold the state in a pointer.
reader *bytes.Reader
}
func NewBlobReader(client *BlobClient, key string) *BlobReader {
return &BlobReader{client: client, key: key}
}
func (r *BlobReader) Read(p []byte) (int, error) {
if r.reader == nil {
data, err := r.client.Fetch(r.key)
if err != nil {
return 0, err
}
// Delegate the complex offset/EOF logic to the standard library
r.reader = bytes.NewReader(data)
}
return r.reader.Read(p)
}
cmd/cli/main.go
package main
import (
"io"
"myapp/internal/storage"
"os"
)
func main() {
client := &storage.BlobClient{}
reader := storage.NewBlobReader(client, "config.json")
// Now works with anything expecting io.Reader
io.Copy(os.Stdout, reader)
}
Practical Example 3 Link to heading
internal/logger/logger.go
package logger
import "log"
// Your interface
type Logger interface {
Info(msg string)
Error(msg string)
}
// Adapter for standard library logger
type StdLogAdapter struct {
logger *log.Logger
}
func NewStdLogAdapter(l *log.Logger) *StdLogAdapter {
return &StdLogAdapter{logger: l}
}
func (a *StdLogAdapter) Info(msg string) {
a.logger.Println("[INFO]", msg)
}
func (a *StdLogAdapter) Error(msg string) {
a.logger.Println("[ERROR]", msg)
}
cmd/cli/main.go
package main
import (
"log"
"myapp/internal/logger"
"os"
)
func main() {
stdLogger := log.New(os.Stdout, "", log.LstdFlags)
// Adapt standard logger to your interface
var l logger.Logger = logger.NewStdLogAdapter(stdLogger)
l.Info("server started")
l.Error("connection failed")
}
Considerations Link to heading
Example 1: http.HandlerFunc is the canonical Go adapter. It’s a type conversion that adapts a function signature to satisfy the http.Handler interface. One line, zero ceremony.
Example 2: This is the standard way to fix broken or foreign interfaces. Wrap the third-party type in a struct that satisfies your interface, and delegate the work. Don’t let foreign types pollute your domain logic.
Example 3: Consumer-defined interface with an adapter for a concrete type. This keeps your code decoupled from specific implementations and makes testing easy. The Adapter pattern is so natural in Go that you often don’t realize you’re using it. Any time you wrap a type to satisfy an interface, you’re adapting.
Verdict Link to heading
- Example 1: Idiomatic
- Example 2: Idiomatic
- Example 3: Idiomatic
Composite Pattern Link to heading
Problem Space Link to heading
The Composite pattern allows you to treat individual objects and collections of objects uniformly through a shared interface. This is particularly useful for tree structures where you want to perform operations across the whole hierarchy without caring whether you’re dealing with a leaf or a branch.
Practical Example 1 Link to heading
cmd/multiwriter/main.go
package main
import (
"bytes"
"io"
"os"
)
func main() {
var buf bytes.Buffer
f, err := os.Create("log.txt")
if err != nil {
panic(err)
}
defer f.Close()
// Composite: multiple writers treated as one
multi := io.MultiWriter(os.Stdout, &buf, f)
io.WriteString(multi, "hello\n") // writes to all three
}
Practical Example 2 Link to heading
internal/notifier/notifier.go
package notifier
type EmailNotifier struct {
address string
}
func NewEmailNotifier(address string) *EmailNotifier {
return &EmailNotifier{address: address}
}
func (e *EmailNotifier) Send(msg string) error {
// send email
return nil
}
type SMSNotifier struct {
phone string
}
func NewSMSNotifier(phone string) *SMSNotifier {
return &SMSNotifier{phone: phone}
}
func (s *SMSNotifier) Send(msg string) error {
// send sms
return nil
}
cmd/cli/main.go
package main
import (
"errors"
"myapp/internal/notifier"
)
type Sender interface {
Send(msg string) error
}
type GroupNotifier struct {
senders []Sender
}
func (g *GroupNotifier) Add(senders ...Sender) {
g.senders = append(g.senders, senders...)
}
func (g *GroupNotifier) Send(msg string) error {
var errs []error
for _, s := range g.senders {
errs = append(errs, s.Send(msg))
}
// "Composite" error handling: treat multiple errors as one
return errors.Join(errs...)
}
func main() {
oncall := &GroupNotifier{}
oncall.Add(
notifier.NewEmailNotifier("oncall@example.com"),
notifier.NewSMSNotifier("555-1234"),
)
managers := &GroupNotifier{}
managers.Add(notifier.NewEmailNotifier("boss@example.com"))
everyone := &GroupNotifier{}
everyone.Add(oncall, managers)
everyone.Send("server is down")
}
Considerations Link to heading
Example 1: io.MultiWriter returns an io.Writer that contains multiple io.Writers. The composite implements the same interface as its leaves, so callers don’t know or care that they’re writing to multiple destinations.
Example 2: The library exports concrete types. The consumer defines the interface it needs. This is the most idiomatic Go approach for behavioral dependencies.
Verdict Link to heading
- Example 1: Idiomatic
- Example 2: Idiomatic
Decorator Pattern Link to heading
Problem Space Link to heading
The Decorator pattern attaches additional behavior to an object dynamically without modifying its structure. Instead of inheritance, you wrap an object with another that implements the same interface and adds functionality. In Go, this is extremely common. Middleware is just decoration.
Practical Example 1 Link to heading
cmd/server/main.go
package main
import (
"log"
"net/http"
"time"
)
func logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
})
}
func auth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") == "" {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
func hello(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello"))
}
func main() {
handler := http.HandlerFunc(hello)
// Decorate: logging runs first, then auth, then hello
decorated := logging(auth(handler))
http.Handle("/", decorated)
http.ListenAndServe(":8080", nil)
}
Practical Example 2 Link to heading
internal/storage/storage.go
package storage
import "fmt"
type Cache interface {
Get(key string) ([]byte, error)
Set(key string, value []byte) error
}
type memoryCache struct {
data map[string][]byte
}
func NewMemoryCache() *memoryCache {
return &memoryCache{data: make(map[string][]byte)}
}
func (c *memoryCache) Get(key string) ([]byte, error) {
v, ok := c.data[key]
if !ok {
return nil, fmt.Errorf("key not found: %s", key)
}
return v, nil
}
func (c *memoryCache) Set(key string, value []byte) error {
c.data[key] = value
return nil
}
internal/storage/decorators.go
package storage
import (
"log"
"time"
)
// Logging decorator
type loggingCache struct {
wrapped Cache
}
func WithLogging(c Cache) Cache {
return &loggingCache{wrapped: c}
}
func (c *loggingCache) Get(key string) ([]byte, error) {
log.Printf("cache get: %s", key)
return c.wrapped.Get(key)
}
func (c *loggingCache) Set(key string, value []byte) error {
log.Printf("cache set: %s", key)
return c.wrapped.Set(key, value)
}
// Metrics decorator
type metricsCache struct {
wrapped Cache
}
func WithMetrics(c Cache) Cache {
return &metricsCache{wrapped: c}
}
func (c *metricsCache) Get(key string) ([]byte, error) {
start := time.Now()
v, err := c.wrapped.Get(key)
log.Printf("cache.Get took %v", time.Since(start))
return v, err
}
func (c *metricsCache) Set(key string, value []byte) error {
start := time.Now()
err := c.wrapped.Set(key, value)
log.Printf("cache.Set took %v", time.Since(start))
return err
}
cmd/cli/main.go
package main
import "myapp/internal/storage"
func main() {
cache := storage.NewMemoryCache()
// Stack decorators: metrics wraps logging wraps cache
decorated := storage.WithMetrics(storage.WithLogging(cache))
decorated.Set("user:1", []byte("alice"))
decorated.Get("user:1")
}
Practical Example 3 Link to heading
cmd/reader/main.go
package main
import (
"bufio"
"compress/gzip"
"io"
"os"
)
func main() {
f, err := os.Open("data.gz")
if err != nil {
panic(err)
}
defer f.Close()
// Stack decorators: buffered reading of gzipped file
// f implements io.Reader
// gzip.NewReader wraps it, returns io.Reader
// bufio.NewReader wraps that, returns io.Reader
gzReader, err := gzip.NewReader(f)
if err != nil {
panic(err)
}
buffered := bufio.NewReader(gzReader)
io.Copy(os.Stdout, buffered)
}
Considerations Link to heading
Example 1: HTTP middleware is the most common Decorator in Go. Each middleware wraps the next handler, adds behavior (logging, auth, recovery), and calls the wrapped handler.
Example 2: Consumer-defined interface with stackable decorators. Each decorator implements the same interface and wraps another implementation. You can compose them in any order.
Example 3: The standard library uses this pattern extensively. bufio.Reader, gzip.Reader, io.TeeReader, io.LimitReader are all decorators that wrap io.Reader and add behavior.
The key insight: if you’re wrapping an interface implementation with another implementation of the same interface to add behavior, you’re using Decorator.
Verdict Link to heading
- Example 1: Idiomatic
- Example 2: Idiomatic
- Example 3: Idiomatic
Facade Pattern Link to heading
Problem Space Link to heading
The Facade pattern provides a simplified interface to a complex subsystem. Instead of exposing multiple components and forcing the caller to coordinate them, you provide a single entry point that handles the orchestration internally.
Practical Example 1 Link to heading
cmd/client/main.go
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
// http.Get is a facade over:
// - creating a Client
// - creating a Request
// - setting headers
// - executing the request
// - handling redirects
resp, err := http.Get("https://example.com")
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
}
Practical Example 2 Link to heading
internal/order/order.go
package order
import "fmt"
// Subsystem: Inventory
type inventory struct{}
func (i *inventory) Check(productID string, qty int) bool {
fmt.Printf("checking inventory for %s\n", productID)
return true
}
func (i *inventory) Reserve(productID string, qty int) error {
fmt.Printf("reserving %d of %s\n", qty, productID)
return nil
}
// Subsystem: Payment
type payment struct{}
func (p *payment) Authorize(amount float64) (string, error) {
fmt.Printf("authorizing payment of $%.2f\n", amount)
return "txn-123", nil
}
func (p *payment) Capture(txnID string) error {
fmt.Printf("capturing transaction %s\n", txnID)
return nil
}
// Subsystem: Shipping
type shipping struct{}
func (s *shipping) CreateLabel(address string) (string, error) {
fmt.Printf("creating shipping label for %s\n", address)
return "tracking-456", nil
}
// Facade
type OrderService struct {
inventory *inventory
payment *payment
shipping *shipping
}
func NewOrderService() *OrderService {
return &OrderService{
inventory: &inventory{},
payment: &payment{},
shipping: &shipping{},
}
}
func (o *OrderService) PlaceOrder(productID string, qty int, amount float64, address string) (string, error) {
if !o.inventory.Check(productID, qty) {
return "", fmt.Errorf("insufficient inventory")
}
if err := o.inventory.Reserve(productID, qty); err != nil {
return "", err
}
txnID, err := o.payment.Authorize(amount)
if err != nil {
return "", err
}
if err := o.payment.Capture(txnID); err != nil {
return "", err
}
trackingID, err := o.shipping.CreateLabel(address)
if err != nil {
return "", err
}
return trackingID, nil
}
cmd/cli/main.go
package main
import (
"fmt"
"myapp/internal/order"
)
func main() {
// Caller doesn't need to know about inventory, payment, or shipping
svc := order.NewOrderService()
trackingID, err := svc.PlaceOrder("SKU-001", 2, 99.99, "123 Main St")
if err != nil {
panic(err)
}
fmt.Printf("order placed, tracking: %s\n", trackingID)
}
Considerations Link to heading
Example 1: http.Get is a facade over the entire HTTP client subsystem. One function call hides client creation, request construction, and execution. The standard library is full of these convenience functions.
Example 2: The order service coordinates three subsystems. Callers don’t need to understand the sequence of inventory checks, payment authorization, and shipping label creation. The facade handles orchestration.
Facade is less about interface satisfaction and more about API design. If you’re hiding complexity behind a simpler API, you’re using Facade.
Verdict Link to heading
- Example 1: Idiomatic
- Example 2: Idiomatic
Flyweight Pattern Link to heading
Problem Space Link to heading
The Flyweight pattern minimizes memory usage by sharing common state between multiple objects. Instead of each object storing its own copy of repeated data, they share a reference to a single instance. This is useful when you have many objects with overlapping data.
Practical Example 1 Link to heading
cmd/pool/main.go
package main
import (
"bytes"
"sync"
)
var bufferPool = sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
}
func process(data string) string {
// Get a buffer from the pool (shared, reused)
buf := bufferPool.Get().(*bytes.Buffer)
buf.Reset()
defer bufferPool.Put(buf)
buf.WriteString("processed: ")
buf.WriteString(data)
return buf.String()
}
func main() {
// Each call reuses buffers instead of allocating new ones
result1 := process("first")
result2 := process("second")
result3 := process("third")
println(result1)
println(result2)
println(result3)
}
Practical Example 2 Link to heading
internal/game/sprite.go
package game
import "fmt"
// Shared intrinsic state (the flyweight)
type Sprite struct {
texture []byte // large, shared across all instances
width int
height int
}
var spriteCache = make(map[string]*Sprite)
func GetSprite(name string) *Sprite {
if s, ok := spriteCache[name]; ok {
return s
}
// Load once, share forever
s := &Sprite{
texture: loadTexture(name), // expensive
width: 64,
height: 64,
}
spriteCache[name] = s
return s
}
func loadTexture(name string) []byte {
fmt.Printf("loading texture: %s\n", name)
return make([]byte, 1024*1024) // 1MB texture
}
// Extrinsic state (unique per instance)
type Enemy struct {
sprite *Sprite // shared
x, y float64 // unique per enemy
health int // unique per enemy
}
func NewEnemy(spriteName string, x, y float64) *Enemy {
return &Enemy{
sprite: GetSprite(spriteName), // shared flyweight
x: x,
y: y,
health: 100,
}
}
cmd/game/main.go
package main
import "myapp/internal/game"
func main() {
// 1000 enemies, but only one "goblin" texture in memory
enemies := make([]*game.Enemy, 1000)
for i := range enemies {
enemies[i] = game.NewEnemy("goblin", float64(i*10), float64(i*5))
}
// Only loads texture once
boss := game.NewEnemy("goblin", 500, 500)
_ = boss
}
Considerations Link to heading
Example 1: sync.Pool is the standard library’s flyweight for temporary objects. Instead of allocating and garbage collecting buffers constantly, you reuse them. This is common in high-throughput servers.
Example 2: Game sprites separate intrinsic state (texture data shared by all goblins) from extrinsic state (position and health unique to each enemy). A thousand enemies share one texture. In a real application with concurrent access you would want to add in a sync.RWMutex or sync.Map.
The pattern splits object state into intrinsic (shared, immutable) and extrinsic (unique, per-instance). The cache or pool manages the shared instances.
Verdict Link to heading
- Example 1: Idiomatic
- Example 2: Idiomatic
Proxy Pattern Link to heading
Problem Space Link to heading
The Proxy pattern provides a surrogate or placeholder for another object to control access to it. Common uses include lazy initialization, access control, logging, and caching. The proxy implements the same interface as the real object, so callers don’t know they’re talking to a proxy.
Practical Example 1 Link to heading
internal/database/database.go
package database
import (
"database/sql"
"fmt"
"sync"
)
type DB interface {
Query(query string, args ...any) (*sql.Rows, error)
Close() error
}
// Real implementation
type realDB struct {
conn *sql.DB
}
func newRealDB(dsn string) (*realDB, error) {
conn, err := sql.Open("postgres", dsn)
if err != nil {
return nil, err
}
return &realDB{conn: conn}, nil
}
func (db *realDB) Query(query string, args ...any) (*sql.Rows, error) {
return db.conn.Query(query, args...)
}
func (db *realDB) Close() error {
return db.conn.Close()
}
// Lazy proxy - only connects when first used
type LazyDB struct {
dsn string
real *realDB
once sync.Once
initErr error
}
func NewLazyDB(dsn string) *LazyDB {
return &LazyDB{dsn: dsn}
}
func (db *LazyDB) init() {
db.once.Do(func() {
fmt.Println("connecting to database...")
db.real, db.initErr = newRealDB(db.dsn)
})
}
func (db *LazyDB) Query(query string, args ...any) (*sql.Rows, error) {
db.init()
if db.initErr != nil {
return nil, db.initErr
}
return db.real.Query(query, args...)
}
func (db *LazyDB) Close() error {
if db.real != nil {
return db.real.Close()
}
return nil
}
cmd/cli/main.go
package main
import "myapp/internal/database"
func main() {
// No connection made yet
db := database.NewLazyDB("postgres://localhost/myapp")
defer db.Close()
// Connection happens here, on first use
rows, err := db.Query("SELECT id FROM users")
if err != nil {
panic(err)
}
defer rows.Close()
}
Practical Example 2 Link to heading
internal/api/api.go
package api
import "fmt"
type Service interface {
GetData(id string) (string, error)
DeleteData(id string) error
}
// Real implementation
type realService struct{}
func NewService() *realService {
return &realService{}
}
func (s *realService) GetData(id string) (string, error) {
return fmt.Sprintf("data for %s", id), nil
}
func (s *realService) DeleteData(id string) error {
fmt.Printf("deleted %s\n", id)
return nil
}
// Access control proxy
type SecureService struct {
real Service
role string
}
func NewSecureService(real Service, role string) *SecureService {
return &SecureService{real: real, role: role}
}
func (s *SecureService) GetData(id string) (string, error) {
// Everyone can read
return s.real.GetData(id)
}
func (s *SecureService) DeleteData(id string) error {
// Only admins can delete
if s.role != "admin" {
return fmt.Errorf("permission denied: %s cannot delete", s.role)
}
return s.real.DeleteData(id)
}
cmd/cli/main.go
package main
import (
"fmt"
"myapp/internal/api"
)
func main() {
svc := api.NewService()
// User proxy
userSvc := api.NewSecureService(svc, "user")
data, _ := userSvc.GetData("123")
fmt.Println(data)
err := userSvc.DeleteData("123")
fmt.Println(err) // permission denied
// Admin proxy
adminSvc := api.NewSecureService(svc, "admin")
err = adminSvc.DeleteData("123")
fmt.Println(err) // nil
}
Considerations Link to heading
Example 1: Lazy proxy defers expensive initialization until the object is actually used. Useful for database connections, file handles, or any resource you might not need on every code path.
Example 2: Protection proxy adds access control without modifying the real implementation. The proxy checks permissions before delegating to the wrapped service.
Proxy is similar to Decorator, but the intent differs. Decorator adds behavior. Proxy controls access. In practice, the implementation often looks the same: wrap an interface, delegate calls.
Verdict Link to heading
- Example 1: Idiomatic
- Example 2: Idiomatic