The problem with giant functions
You built a simple HTTP handler that returns JSON. It works. Now the product manager wants authentication, request logging, and rate limiting. You could jam all that logic into one giant function. The handler grows to two hundred lines, mixing business logic with infrastructure concerns. Or you could copy-paste the handler three times, creating HandlerWithAuth, HandlerWithLogging, and HandlerWithAuthAndLogging. Both options create a maintenance nightmare.
Go offers a cleaner path. You keep the handler simple. You wrap it. Each wrapper adds one slice of behavior, and you stack them like layers. The outermost wrapper handles the request first, then passes control inward. The result bubbles back out through the stack. This is the decorator pattern. In Go, it lives naturally through interfaces and composition. You don't need inheritance. You don't need copy-pasting. You just wrap the interface.
What a decorator actually is
A decorator is a struct that implements the same interface as the thing it wraps. It holds a reference to the wrapped object inside a field. When you call a method on the decorator, it can do extra work before or after passing the call down to the wrapped object.
Think of a coffee order. The base is a simple coffee. You can wrap it in a milk decorator. You can wrap that in a sugar decorator. You can wrap that in a foam decorator. Each wrapper knows how to calculate its own cost and description, and each wrapper delegates the rest to the inner coffee. The customer sees one object. The code sees a chain of structs, each satisfying the same interface.
Go doesn't have class inheritance. You can't extend a class to add behavior. Instead, you compose. You build a chain of structs. The interface is the contract. The decorator is the extension. This approach keeps types small and focused. It also lets you mix and match behaviors at runtime.
Minimal example
Here's the simplest decorator: a coffee interface, a base coffee, and a milk wrapper that adds cost and description.
package main
import "fmt"
// Coffee defines the contract for any drinkable item.
// Every decorator must implement this interface.
type Coffee interface {
Cost() int
Description() string
}
// SimpleCoffee implements the base behavior.
// It has no dependencies and returns fixed values.
type SimpleCoffee struct{}
// Cost returns the base price.
func (s *SimpleCoffee) Cost() int { return 5 }
// Description returns the base name.
func (s *SimpleCoffee) Description() string { return "Simple Coffee" }
// MilkDecorator wraps a Coffee to add milk behavior.
// It holds the wrapped coffee in the Coffee field.
type MilkDecorator struct {
Coffee Coffee
}
// Cost delegates to the wrapped coffee and adds the milk cost.
// The decorator adds its own value to the underlying result.
func (m *MilkDecorator) Cost() int { return m.Coffee.Cost() + 2 }
// Description appends milk to the wrapped description.
// It calls the inner description and concatenates the modifier.
func (m *MilkDecorator) Description() string { return m.Coffee.Description() + ", Milk" }
func main() {
// Assign SimpleCoffee to the Coffee interface variable.
// The interface holds the type and pointer to the struct.
var c Coffee = &SimpleCoffee{}
// Wrap the coffee in MilkDecorator.
// The variable c now holds a decorator, not the simple coffee.
c = &MilkDecorator{Coffee: c}
// Call Cost. The decorator adds 2 to the underlying cost.
// The call chain goes: MilkDecorator -> SimpleCoffee.
fmt.Println(c.Description(), c.Cost())
}
The output is Simple Coffee, Milk 7. The decorator added its cost and description on top of the base. You can stack more decorators by repeating the assignment. c = &SugarDecorator{Coffee: c} adds another layer. The interface variable c always points to the outermost wrapper. The chain grows inward.
How the compiler and runtime handle it
Go uses implicit interface satisfaction. You don't declare that MilkDecorator implements Coffee. The compiler checks the methods. If MilkDecorator has Cost() int and Description() string, it satisfies the interface. This keeps the code flexible. You can wrap any type that matches the signature, even types from third-party packages.
At runtime, an interface value is a pair: a type and a value. When you assign &SimpleCoffee{} to c, the interface holds the type *SimpleCoffee and the pointer. When you assign &MilkDecorator{Coffee: c}, the interface holds the type *MilkDecorator and the pointer to the decorator. The decorator struct contains the Coffee field, which holds the inner interface. This creates a linked list of interface values.
Method calls follow the chain. c.Cost() calls MilkDecorator.Cost. That method accesses m.Coffee, which is the interface holding SimpleCoffee. It calls Coffee.Cost() on that interface, which dispatches to SimpleCoffee.Cost. The result bubbles back up. Each layer adds its work. The indirection has a tiny cost, but it's negligible for most applications. The flexibility is worth it.
Convention aside: receiver names should be short. Use (m *MilkDecorator) or (c *Coffee). Don't use (this *MilkDecorator) or (self *MilkDecorator). Go idioms favor brevity. The type name is already in the signature. The receiver name just needs to be distinct.
Realistic example: Counting bytes
Coffee is a teaching tool. Real Go code uses decorators everywhere, especially in the io package. The io.Reader interface is the canonical decorator target. You can wrap a file, a network connection, or a string reader with decorators that add buffering, compression, encryption, or counting.
Here's a decorator that wraps any reader and counts how many bytes it consumes. It implements io.Reader, so it works with any function that expects a reader.
package main
import (
"bytes"
"fmt"
"io"
)
// CountingReader wraps an io.Reader and tracks bytes read.
// It implements io.Reader so it can be used anywhere a reader is expected.
type CountingReader struct {
r io.Reader
count int64
}
// Read delegates to the wrapped reader and increments the counter.
// It returns the count and error from the underlying read.
func (cr *CountingReader) Read(p []byte) (int, error) {
n, err := cr.r.Read(p)
// Increment the counter by the number of bytes actually read.
// Even on error, some bytes might have been read.
cr.count += int64(n)
return n, err
}
// Count returns the total bytes consumed so far.
// This method is not part of io.Reader; it's extra behavior.
func (cr *CountingReader) Count() int64 { return cr.count }
func main() {
// Create a simple reader from a string.
src := bytes.NewReader([]byte("Hello, Go decorators!"))
// Wrap the reader in CountingReader.
// The variable reader holds the decorator, not the bytes.Reader.
reader := &CountingReader{r: src}
// Read all data using io.ReadAll.
// The decorator intercepts every Read call.
data, _ := io.ReadAll(reader)
// Print the data and the count tracked by the decorator.
fmt.Printf("Data: %s\n", data)
fmt.Printf("Bytes read: %d\n", reader.Count())
}
The output shows the data and the byte count. The io.ReadAll function doesn't know about counting. It just calls Read. The decorator intercepts the call, delegates to the source, updates its state, and returns. This is the power of composition. You added behavior without modifying io.ReadAll or bytes.Reader. You just wrapped the interface.
Convention aside: io.Reader is the most important interface in Go. Many packages accept io.Reader and return io.Reader. This design enables chaining. You can wrap a reader in a gzip decompressor, then wrap that in a counting reader, then wrap that in a buffered reader. The chain handles decompression, counting, and buffering in one pass.
Pitfalls and errors
Decorators are simple, but they have traps. The most common bug is infinite recursion. If you accidentally wrap the decorator with itself, the chain loops forever.
// BAD: Infinite recursion
d := &CountingReader{r: d}
The compiler won't catch this. The variable d is initialized to a struct containing d. The runtime panics with runtime error: invalid memory address or nil pointer dereference when the first read happens, or it causes a stack overflow if the recursion triggers before the nil check. Always initialize the wrapped field with a distinct object.
Another pitfall is wrapping a nil interface. If you pass nil to the decorator, the delegation call fails.
// BAD: Wrapping nil
var r io.Reader
cr := &CountingReader{r: r}
cr.Read(buf) // Panics
The runtime panics with runtime error: invalid memory address or nil pointer dereference because cr.r is nil. The decorator should validate the input or handle nil gracefully. In production code, check for nil before wrapping, or document that nil is not allowed.
Interface mismatches also cause errors. If you forget a method in the decorator, the compiler rejects the code.
The compiler rejects this with
cannot use &MilkDecorator{} as Coffee value in assignment: *MilkDecorator does not implement Coffee (missing Cost method).
This error is helpful. It tells you exactly which method is missing. Fix the method signature, and the code compiles. Go's strict interface checking prevents silent failures.
Convention aside: error handling is verbose by design. If your decorator wraps an operation that returns an error, you must check it. if err != nil { return err }. The community accepts the boilerplate because it makes the unhappy path visible. Don't swallow errors in a decorator. Propagate them up the chain.
When to use decorators
Go gives you several ways to add behavior. Decorators are one tool. Pick the right one for the job.
Use a decorator when you need to add behavior dynamically without modifying the original type. Decorators let you wrap existing code, including third-party types, and extend them at runtime.
Use a decorator when you want to compose multiple behaviors in a stack. Stacking decorators creates a pipeline where each layer handles a specific concern. This keeps code modular and testable.
Use a struct decorator when the wrapper needs state. If the behavior requires tracking data, like a byte counter or a cache, a struct holds the state naturally.
Use a functional decorator when the wrapper is stateless and you want concise syntax. A function that takes a handler and returns a handler is shorter than defining a struct. This is common in HTTP middleware.
Use a simple struct field when the behavior is static and known at design time. If you always need logging, add a logger field to the struct. Don't over-engineer with decorators for fixed requirements.
Use plain sequential code when you don't need composition. The simplest thing that works is usually the right thing. Decorators add indirection. Avoid them if the logic fits in one function.
Decorators are composition in action. Wrap, don't inherit. The interface is the contract. The decorator is the extension. Trust the type system. Stack layers. Keep each layer thin.