{} The Go Reference

Structural pattern · Gang of Four · Intermediate

Decorator

Attach new behavior to an object by wrapping it in another object of the same interface — without changing the original's type.

Also known as — Wrapper

Structural Intermediate ⏱ 4 min read Complete

☕ Analogy

You order a dark roast. Add a shot of mocha — the price goes up and the name grows. Add whip, then soy. At each step you still have “a beverage” you can ask for a description and a cost, but it’s been wrapped with one more topping. You never defined a DarkRoastWithMochaAndWhipAndSoy class — you just kept wrapping.

The problem

You want to add features to objects in arbitrary combinations. Modelling each combination as its own type is hopeless: with five condiments you’d need dozens of classes. Decorator instead makes each feature a wrapper that holds a beverage and adds to it, so combinations are built at runtime by nesting.

Structure

classDiagram
class Beverage {
  <<interface>>
  +Description() string
  +Cost() float64
}
class DarkRoast {
  +Description() string
  +Cost() float64
}
class Mocha {
  -component Beverage
  +Description() string
  +Cost() float64
}
Beverage <|.. DarkRoast
Beverage <|.. Mocha
Mocha o--> Beverage : wraps

A decorator (Mocha) both implements Beverage and holds a Beverage. That dual role is what lets you stack wrappers indefinitely.

How it works

Each layer delegates to the one it wraps, then adds its own contribution:

graph LR
D["DarkRoast<br/>$1.99"] --> M["+ Mocha<br/>+$0.20"] --> M2["+ Mocha<br/>+$0.20"] --> W["+ Whip<br/>+$0.10"] --> T["= $2.49"]

Idiomatic Go

A common version uses an explicit component Beverage field. Go’s struct embedding makes it even tighter — embed the interface and override only Description and Cost. Edit and Run:

decorator.go — editable & runnable
package main

import "fmt"

type Beverage interface {
Description() string
Cost() float64
}

// Base component.
type DarkRoast struct{}

func (DarkRoast) Description() string { return "Dark Roast" }
func (DarkRoast) Cost() float64       { return 1.99 }

// Mocha embeds a Beverage and decorates it.
type Mocha struct{ Beverage }

func (m Mocha) Description() string { return m.Beverage.Description() + ", Mocha" }
func (m Mocha) Cost() float64       { return m.Beverage.Cost() + 0.20 }

// Whip is another decorator.
type Whip struct{ Beverage }

func (w Whip) Description() string { return w.Beverage.Description() + ", Whip" }
func (w Whip) Cost() float64       { return w.Beverage.Cost() + 0.10 }

func main() {
var b Beverage = DarkRoast{}
b = Mocha{b}
b = Mocha{b} // double mocha
b = Whip{b}
fmt.Printf("%s = $%.2f\n", b.Description(), b.Cost())
}

🐹 Decorators are everywhere in Go

The io package is built on decorators: gzip.NewWriter(f) wraps an io.Writer to add compression, bufio.NewWriter(gz) wraps that to add buffering — each keeps the io.Writer interface. The same idea drives HTTP middleware: func(next http.Handler) http.Handler wraps a handler with logging, auth, or recovery while still being an http.Handler.

Decorator as HTTP middleware

The most common Decorator you’ll write in Go is middleware: a func(http.Handler) http.Handler that wraps a handler with logging, auth, or recovery while still being an http.Handler. Stacking them is decorator nesting. This runs in-process (no real network needed):

middleware.go — editable & runnable
package main

import (
"fmt"
"net/http"
"net/http/httptest"
)

// A Decorator: take a Handler, return a Handler with extra behavior.
func Logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	fmt.Println("->", r.Method, r.URL.Path)
	next.ServeHTTP(w, r) // delegate to the wrapped handler
	fmt.Println("<- done")
})
}

func AddHeader(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("X-Demo", "yes")
	next.ServeHTTP(w, r)
})
}

func main() {
var h http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "hello")
})
h = AddHeader(h) // inner
h = Logging(h)   // outer — runs first

rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest("GET", "/hi", nil))
fmt.Println("status:", rec.Code, "X-Demo:", rec.Header().Get("X-Demo"))
}

Each layer keeps the http.Handler contract and adds one concern — exactly the io.Writer decorator stack (gzipbufio), applied to requests.

In the standard library

  • compress/gzip.NewWriter, bufio.NewReader/Writer — wrap an io.Reader/io.Writer.
  • io.MultiWriter, io.LimitReader, io.TeeReader — readers/writers that decorate others.
  • net/http middleware — handler wrappers for logging, auth, gzip, recovery.

Pitfalls

⚠️ Order matters, and depth hurts

Wrapping order changes behavior — gzip-then-buffer is not the same as buffer-then-gzip. And a tower of ten wrappers makes stack traces and debugging painful. Use decorators for genuinely composable, independent features; if the layering is fixed, a single type is clearer.

When to use it — and when not

✅ Reach for it when

  • You want to add responsibilities to objects dynamically, in combinations, at runtime.
  • Subclassing every combination would explode (DarkRoastWithMochaAndWhipAndSoy…).
  • You want to keep the base type closed for modification but open for extension.

⛔ Think twice when

  • There's exactly one fixed extension — just put it in the type.
  • Deeply nested wrappers make stack traces and debugging hard to follow.
  • Behavior depends on the *order* of wrapping in ways that surprise callers.

Check your understanding

Score: 0 / 5

1. How does Decorator differ from Adapter?

A decorator is a wrapper that *is-a* the thing it wraps (same interface) and enriches it. An adapter is a wrapper that makes an object look like a *different* interface.

2. What Go feature makes decorators especially concise?

Embedding the wrapped interface promotes all its methods automatically, so a decorator only writes the methods it actually modifies.

3. Which is a Decorator in Go's standard library?

gzip.NewWriter wraps an io.Writer and returns an io.Writer that compresses — same interface, added behavior. bufio.NewReader and io.MultiWriter are the same idea.

4. Which design principle does Decorator most embody?

Decorator lets the base type stay closed for modification while remaining open for extension: new responsibilities arrive as new wrapper types, so you never reopen DarkRoast to add Mocha.

5. In `h = Logging(AddHeader(handler))`, which middleware's code runs first on a request?

Each wrapper runs its 'before' code, calls next.ServeHTTP, then its 'after' code. Execution unwinds outermost-first on the way in and innermost-first on the way out — so wrapping order is the request pipeline, and getting it wrong (auth after logging body, say) is a real bug.

Comments

Sign in with GitHub to join the discussion.