Buys independently composable cross-cutting behaviour with no edits to the wrapped code; pays in order-sensitivity the compiler won't catch and opaque stack traces.

The Decorator pattern wraps an object to add behaviour, keeping the same interface. In Go, this pattern is everywhere: it's how HTTP middleware works. Any function that takes an interface and returns the same interface, adding behaviour in between, is a decorator.

The canonical Go example is http.Handler middleware: a function that takes a handler, returns a new handler that logs, authenticates, compresses, or rate-limits, and then calls the original.

It's the Open/Closed Principle.

Scenario#

You have an HTTP handler that serves an API. You need to add logging. Then authentication. Then CORS headers. Each concern is independent, but you don't want to stuff all of them into one giant handler. And you want to compose them differently for different routes.

go
// fat_handler.go
package api

import (
    "log"
    "net/http"
    "time"
)

func handleItems(w http.ResponseWriter, r *http.Request) {
    // Authentication check (shouldn't be here)
    token := r.Header.Get("Authorization")
    if token == "" {
        http.Error(w, "unauthorised", 401)
        return
    }

    // Logging (shouldn't be here)
    start := time.Now()
    defer func() {
        log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
    }()

    // CORS (shouldn't be here)
    w.Header().Set("Access-Control-Allow-Origin", "*")

    // Actual response — buried under cross-cutting concerns
    w.Write([]byte("items: []"))
}

Every cross-cutting concern is tangled into the handler. Want logging on another route? Copy-paste. Want auth on some routes but not others? Conditionals. The response logic is obscured by plumbing.

Solution#

Each concern becomes a middleware function: it takes an http.Handler, returns a new http.Handler that adds one behaviour, and calls the original. Stack them like function composition.

code
Request ──► Logging ──► Auth ──► CORS ──► Handler
               │           │        │         │
             wraps       wraps    wraps     actual
            handler     handler  handler   logic

Each layer: func(http.Handler) http.Handler

Run it to watch each request pass through the wrapper stack — authenticated, rejected, and a route with no auth at all:

main.go
package main

import (
	"fmt"
	"log"
	"net/http"
	"net/http/httptest"
	"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, "unauthorised", http.StatusUnauthorized)
			return
		}
		next.ServeHTTP(w, r)
	})
}

func CORS(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Access-Control-Allow-Origin", "*")
		next.ServeHTTP(w, r)
	})
}

func itemsHandler(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("items: []"))
}

func healthHandler(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("ok"))
}

func send(handler http.Handler, path, token string) {
	req := httptest.NewRequest("GET", path, nil)
	if token != "" {
		req.Header.Set("Authorization", "Bearer "+token)
	}
	rec := httptest.NewRecorder()
	handler.ServeHTTP(rec, req)
	fmt.Printf("%s %d %s\n", path, rec.Code, rec.Body.String())
}

func main() {
	items := Logging(Auth(CORS(http.HandlerFunc(itemsHandler))))
	health := Logging(http.HandlerFunc(healthHandler))

	send(items, "/items", "token")   // 200
	send(items, "/items", "")        // 401 — no auth
	send(health, "/health", "")      // 200 — no auth required
}

Output:

code
/items 200 items: []
/items 401 unauthorised

/health 200 ok

When to Use#

  • You need to add behaviour to objects without modifying their code.
  • You want to compose behaviours independently: different combinations for different cases.
  • The behaviour is cross-cutting (logging, auth, caching, metrics) and shouldn't live in business logic.
  • You see yourself wrapping an http.Handler. You're already using Decorator.

When Not to Use#

  • The added behaviour is tightly coupled to the object's internals. A decorator that needs private fields isn't a decorator; it's a refactoring need.
  • Deep decorator stacks (5+ layers) make debugging difficult. Consider whether a Chain of Responsibility would be clearer.
  • You only ever need one fixed combination. Direct composition in a single handler might be simpler.

The Decision#

The function-wrapper form is idiomatic Go: returning http.HandlerFunc(func(...) {...}) adds almost no boilerplate and every Go developer recognises it instantly. The cost that accumulates is order sensitivity. Logging(Auth(handler)) logs all requests including rejected ones; Auth(Logging(handler)) only logs authenticated traffic. Small difference, large operational impact, and the compiler won't warn you either way.

Stack traces through multiple anonymous closures also become hard to read. When Auth short-circuits inside a chain five wrappers deep, the request path in logs shows nothing useful. Name your handler functions rather than using anonymous closures, and keep chains to three or four layers.

  • Adapter: Adapter resolves an interface mismatch; Decorator keeps the same interface and adds behaviour. If your wrapper changes the API, it's an Adapter; if it preserves the API and enriches it, it's a Decorator.
  • Composite: Decorator wraps exactly one object and adds behaviour; Composite aggregates many objects of the same type. If you wrap one, Decorator; if you compose many, Composite.
  • Proxy: Proxy and Decorator are structurally identical in Go; the distinction is intent. Proxy controls or intercepts access (lazy init, auth, caching); Decorator adds new capabilities without restricting access.
  • Chain of Responsibility: HTTP middleware chains are both Decorator and Chain of Responsibility: each middleware wraps the next (Decorator) and may short-circuit the chain without calling the inner handler (Chain of Responsibility).