Stacked middleware vs embedded delegation in Go
Redowan Delowar
March 6, 2025
Middleware is usually the go-to pattern in Go HTTP servers for tweaking request behavior.
Typically, you wrap your base handler with layers of middleware - one might log every
request, while another intercepts specific routes like /special to serve a custom
response.
However, I often find the indirections introduced by this pattern a bit hard to read and
debug. I recently came across the embedded delegation pattern while browsing [Gin's HTTP
router source code]. Here, I explore both patterns and explain why I usually start with
delegation whenever I need to modify HTTP requests in my Go services.
Middleware stacking
Here's an example where the logging middleware records each request, and the special
middleware intercepts requests to /special:
In this setup, every incoming request is first handled by the special middleware, which
checks for the /special route, and then by the logging middleware that logs the request
details. We're effectively stacking the middleware functions.
If you hit the server with:
the server logs will look like this:
Stacking middleware functions like middleware3(middleware2(middleware1(mux))) can get
messy when you have many of them. That's why people usually write a wrapper function to
apply the middlewares to the mux:
applyMiddleware takes an http.Handler and a variadic list of middleware functions
(...func(http.Handler) http.Handler). It loops over the middleware in reverse order so
each one wraps the next properly. This avoids deep nesting like
middleware3(middleware2(middleware1(mux))) and keeps the middleware chain tidy.
You'd then use it like this:
This behaves just like the manual middleware stacking, but it's a bit cleaner.
While this is the canonical way to handle request-response modifications in Go, it can
sometimes be hard to reason about, especially when debugging or dealing with many middleware
layers.
There's another way to achieve the same result without dealing with a soup of nested
functions. The next section talks about that.
Embedded delegation
Embedded delegation (or the delegation pattern) means you embed the standard HTTP
multiplexer inside your own struct and override its ServeHTTP method.
It's a bit like inheritance - overriding a method in a subclass to add extra functionality
and then delegating the call to the original method. Although Go doesn't have a class
hierarchy, you can still delegate responsibilities to the embedded type's method.
The following example implements the same behavior - logging every request and intercepting
the /special route - directly within a custom mux:
In this example, the custom mux centralizes both logging and special-case route handling
within one ServeHTTP method. This approach cuts out the extra function calls in a
middleware chain and can simplify tracking the request flow. I find it a bit easier on the
eyes too.
If you have a bunch of extra functionality to add inside cm.ServeHTTP, you can wrap them
in utility functions like this:
Then, simply call these functions inside your cm.ServeHTTP method:
This keeps all the request modifications in a single ServeHTTP method.
Mixing the two approaches
You can also mix both techniques. For example, you might use direct delegation for special
route handling and then wrap the resulting handler with middleware for logging. Here's how a
hybrid solution might look:
In this hybrid approach, the specialized behavior (intercepting the /special path) is
handled via direct delegation, while logging stays modular as middleware. This gives you the
best of both worlds.
I usually start with the embedded delegation and gradually introduce the middleware pattern
if I need it later. It's easier to adopt the middleware pattern if you start with delegation
than the other way around.
[gin's http router source code]:
https://github.com/gin-gonic/gin/blob/3b28645dc95d58e0df36b8aff7a6c64f7c0ca5e9/gin.go#L94
Discussion in the ATmosphere