Stacked middleware vs embedded delegation in Go

Redowan Delowar March 6, 2025
Source
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

Loading comments...