9 Composition patterns
This chapter champions Go’s composition-first philosophy: favor small, explicit pieces that can be combined over inheritance or sprawling frameworks. It shows how higher-order functions and implicit interfaces lead to reusable building blocks, and stresses consistent function signatures and explicit dependency injection for clarity and testability. Across HTTP services, it applies these ideas to cross-cutting concerns such as logging, tracing, encoding/decoding, and safe I/O, demonstrating how simple components compose into robust systems.
It begins with the middleware pattern, wrapping http.Handlers to add behavior like request/response logging without touching handler code. Response metrics are gathered via independent, chainable pieces (for example, Duration and StatusCode) collected in a Response value through a RecordResponse helper. To capture status codes, an Interceptor embeds http.ResponseWriter and intercepts WriteHeader. The chapter then explores the hazards of field embedding with optional interfaces (Flusher, Hijacker, Pusher), showing how type assertions and the standard ResponseController preserve optional behavior, and adds an Unwrap method so wrappers can expose the original ResponseWriter when needed.
For request-scoped telemetry, a traceid middleware injects IDs into Context and a custom slog.Handler automatically adds them as log attributes. The handler chaining pattern introduces a chainable hio.Handler and a Responder helper (Error, Redirect, Text, JSON) to enforce explicit returns and prevent subtle response bugs. JSON helpers simplify marshaling and unmarshaling, with validation discovered via anonymous interfaces. Finally, the chapter strengthens reliability by wrapping Request.Body with MaxBytesReader to bound input size and by unwrapping ResponseWriters so servers can disconnect clients that exceed limits—illustrating how careful wrapping, unwrapping, and interface-based composition yield simple, modular, and maintainable code.
Middleware layers process both incoming HTTP requests and outgoing HTTP responses, enhancing functionality without altering the handler code.
The middleware's handler forwards requests to ServeMux and then logs.
The logger middleware calls RecordResponse, which runs the Duration middleware to wrap the handler and calculate response duration. RecordResponse returns response metrics in a Response, which the logger middleware logs.
Interceptor captures HTTP status codes by intercepting WriteHeader.
Injecting and propagating trace IDs through the Request.Context.
Automatically adding log attributes to log records by wrapping the original log handler with a custom one that extracts trace IDs from Context.
Shorten delegates response processing to other handlers in the chain.
Summary
- We can pass functions to functions or return them from functions.
- Middleware adds extra functionality to handlers without modifying their code.
- The canonical middleware signature is func(http.Handler) http.Handler. This signature also allows us to chain multiple middleware (including third-party ones).
- Avoid package-level dependencies for testable and maintainable code.
- Be consistent in your function signatures for better composability.
- Expert packages improve maintainability by keeping related code together.
- Generic packages may introduce unnecessary dependencies, hurting maintenance.
- Use the standard library's slog package for structured logging.
- slog.Attr is a key-value pair to put in a log record. It can contain any value type.
- slog.String returns a slog.Attr value with a string value.
- slog.Any returns a slog.Attr value with any value.
- Use slog.Any to avoid unnecessary String method calls.
- slog.LogAttrs is more efficient than other slog.Logger methods.
- Avoid unnecessary coupling between types to improve composability.
- slices.Backward returns an iterator that iterates over a slice in reverse order.
- Embedding a type without a field name in a struct promotes its fields and methods, allowing automatic forwarding of method calls from the outer type to the inner.
- Embedding an interface makes the embedder satisfy the interface.
- Embedding can surprisingly change the embedder type's behavior.
- Intercept method calls by embedding an interface and add the methods to intercept.
- slog.Int returns a slog.Attr value with an int value.
- Optional interfaces help to detect extra functionality behind an interface.
- Concrete types behind interfaces may have additional methods.
- Type assertions can detect and extract the underlying type of an interface.
- Embedding an interface only adds that interface's methods to the embedder.
- Embedded interfaces can hide the capabilities of their underlying types.
- Implementing the Unwrap method allows ResponseController and handlers to unwrap the underlying ResponseWriter to access its functionalities.
- http.ResponseController is a concrete type that wraps an http.ResponseWriter and allows us to use optional functionalities of the underlying implementation.
- Context can implicitly propagate any value across package boundaries.
- context.WithValue attaches a single value to a new Context.
- Context.Value extracts an any value from a Context using a key.
- Context keys should be unique types to prevent key collisions.
- Context keys are often an unexported type to avoid collisions. Using an empty struct as the key type is common and avoids memory allocations.
- Use Context for storing request-scoped values. The values stored in a Context should inform but not control the program's flow. Storing many values in a Context can lead to memory leaks and make the code harder to maintain and test.
- slog.Logger handles slog.Record values with a slog.Handler.
- slog.Handler is an interface that defines the behavior of handling log records.
- slog.Handler.WithAttrs returns a new Handler that adds the given attributes to the log records. WithGroup returns a new Handler that groups log records.
- HTTP handler chaining patterns reduce the chances of unintentional mistakes, make the code more maintainable, and simplify repetitive tasks.
- Reducing unnecessary direct dependencies may improve modularity and flexibility.
- json.Marshal and json.Unmarshal are functions for encoding and decoding JSON.
- json only considers exported fields while encoding and decoding.
- Reading large payloads could lead to denial-of-service attacks (DoS).
- http.MaxBytesReader helps limit reading excessive request bodies. It can instruct http.Server using a method to disconnect clients that go beyond the limit.
- Type assertion with an anonymous interface that we can declare within a function makes it easier to unearth and use methods from the interface's underlying type.
- Avoid doing the caller's work; provide composable building blocks instead.
- Small, focused functions improve testability, maintainability, and composability.
- Composition reduces code duplication and prevents rigid abstractions.
- Packages that provide composable functionality and building blocks are often more reusable than packages with all-in-one solutions and do everything for users.
FAQ
What is HTTP middleware in Go and what is its idiomatic function signature?
Middleware wraps an http.Handler to add behavior before and/or after the next handler runs. Idiomatic signature:func(next http.Handler) http.Handler. Because it returns a handler, you can chain multiple middleware: h := A(B(mux)). This enables composable pre-/post-processing such as logging, tracing, or metrics without changing the original handlers.Why avoid global loggers and how do I inject a slog.Logger into middleware?
Global loggers make testing and configuration harder. Prefer explicit injection: definefunc Middleware(lg *slog.Logger) func(http.Handler) http.Handler, build the wrapper with your configured logger, then wrap the mux: w := Middleware(lg); srv.Handler = w(mux). This keeps code testable and lets you use lg.LogAttrs efficiently (e.g., slog.Any, slog.String, slog.Duration).How do I log response duration and status code with middleware?
- Create a Response struct (e.g.,Duration time.Duration, StatusCode int).- Implement small middleware building blocks:
- Duration: records
time.Since(start) via defer and stores it via pointer.- StatusCode: wraps the ResponseWriter with an Interceptor and captures codes (defaults to 200).
- Use a helper
RecordResponse(next, w, r) Response that chains these, runs the handler, and returns metrics. The logger then logs slog.Duration("duration", rr.Duration) and slog.Int("status", rr.StatusCode).How does the Interceptor pattern capture HTTP status codes?
Define anInterceptor that embeds http.ResponseWriter and implements WriteHeader. In WriteHeader, call a hook (e.g., OnWriteHeader(code)) to record the status, then forward to the embedded writer. Embedding forwards all other methods automatically; overriding only WriteHeader lets you capture codes transparently.What are ResponseWriter’s optional interfaces and how do I preserve them when wrapping?
Optional interfaces likehttp.Flusher, http.Hijacker, and http.Pusher may exist on the underlying writer. Wrapping can hide them. Solutions:- Add
Unwrap() http.ResponseWriter to your wrapper so others can recover the underlying writer.- Prefer
http.NewResponseController(w) and call methods like Flush(); it recursively searches wrappers and returns http.ErrNotSupported if unavailable.How do I propagate and automatically log trace IDs?
- Use a traceid package:Middleware attaches a generated ID to r.Context() if absent; helpers WithContext/FromContext manage storage.- Wrap the slog handler with
traceid.LogHandler. Its Handle extracts the trace ID from the context and adds trace_id to the record. Implement WithAttrs/WithGroup to return wrapped handlers so the attribute is preserved. Compose: srv.Handler = traceid.Middleware(loggerMiddleware(mux)); create lg := slog.New(traceid.NewLogHandler(base.Handler())).What is the handler chaining pattern (hio.Handler) and why use it?
hio.Handler is func(http.ResponseWriter, *http.Request) hio.Handler. Its ServeHTTP calls the handler; if it returns another handler, it continues until a nil return stops the chain. This forces explicit returns and prevents subtle bugs like continuing after writing an error. Convenience responders (e.g., Responder.Text, Responder.Redirect, Responder.Error) return terminal handlers that end the chain.How can I encode JSON responses and decode/validate JSON requests cleanly?
- Respond:Responder.JSON(code, v) marshals once, sets Content-Type: application/json, writes the status and bytes, then returns nil to end the chain.- Decode:
hio.DecodeJSON(r io.Reader, to any) error reads, json.Unmarshals into to, and if to implements interface{ Validate() error }, calls Validate. Ensure target fields are exported so JSON can set them.How do I protect the server from large request bodies when decoding JSON?
Wrap the body with a limiter and (crucially) pass the original ResponseWriter to support server-side disconnects:-
rc := hio.MaxBytesReader(w, r.Body, 4096)-
err := hio.DecodeJSON(rc, &dst)hio.MaxBytesReader unwraps any ResponseWriter wrappers via Unwrap() in a loop, then calls http.MaxBytesReader. If the limit is exceeded, decoding fails and the server closes the connection.What design principles for composable Go code are emphasized in this chapter?
- Prefer composition over inheritance; keep functions and types small and reusable.- Be consistent with function signatures to enable chaining (e.g.,
func(http.Handler) http.Handler).- Inject dependencies explicitly; avoid globals.
- Use Context to inform, not control, flow; keep values minimal and scoped.
- Treat embedding carefully (method hiding); use type assertions, anonymous interfaces, and unwrapping to preserve/extend behavior.
- Build helper blocks (middleware, responders) and compose them rather than introducing tight coupling.
Go by Example ebook for free