While writing complex services in go, one typical topic that you will encounter is
middleware. This topic has been discussed
again, and
again, and
again, on internet. Essentially a middleware should allow us to:
- Intercept a
ServeHTTP
call, and execute any arbitrary code.
- Make changes to request/response flow along continuation chain.
- Break the middleware chain, or continue onto next middleware interceptor eventually leading to real request handler.
All of this would sound very similar to what
express.js middleware do. We explored
various libraries and
found existing solutions that closely matched what we wanted, but they either had
unnecessary extras, or were
not delightful for our taste buds. It was pretty obvious that we can write
express.js inspired middleware, with cleaner installation API under 20 lines of code ourselves.
The abstraction
While designing the abstraction, we first imagined how we want to write our middleware functions (referred as interceptors from this point onwards), and the answer was pretty obvious:
https://gist.github.com/x0a1b/f7c64c92cfc6eae7ba1c2931bbabb475#file-middleware_example-go
They look just like
http.HandlerFunc
, but having an extra parameter
next
that continues the handler chain. This would allow anyone to write interceptor as simple function similar to
http.HandlerFunc
that can intercept the call, do what they want, and pass on the control if they want to.
Next we imagined how to hook these interceptors to our
http.Handler
or
http.HandlerFunc
. In order to do so, first thing do is define
MiddlewareHandlerFunc
which is simply a type of
http.HandlerFunc
(i.e.
type MiddlewareHandlerFunc http.HandlerFunc
). This will allow us to build a nicer API on top of stock
http.HandlerFunc
. Now given a
http.HandlerFunc
we want our chain-able API to look somewhat like this:
https://gist.github.com/x0a1b/f7c64c92cfc6eae7ba1c2931bbabb475#file-usage_example-go
Casting
http.HandlerFunc
to
MiddlewareHandlerFunc
, and then calling
Intercept
method to install our interceptor. The return type of
Intercept
is again a
MiddlewareHandlerFunc
, which allows us to call
Intercept
again.
One important thing to note with
Intercept
scheme is the order of execution. Since calling
chain(responseWriter, request)
is indirectly invoking last interceptor, the execution of interceptors is reversed i.e. it goes from interceptor at tail all the way back to handler at head. Which makes perfect sense because you are
intercepting the call; so you should get a chance to execute before your parent.
The simplification
While this reverse chaining system makes the abstraction more fluent, turns out most the time we have a precompiled array of interceptors that will be reused with different handlers. Also when we are defining a chain of middleware as an array, we would naturally prefer to declare them in the order of their execution (not reversed order). Let's call this array interceptors
MiddlewareChain
. We want our middleware chains to look somewhat like:
https://gist.github.com/x0a1b/f7c64c92cfc6eae7ba1c2931bbabb475#file-array_usage_example-go
Note these middleware will be invoked in same order as the appear in the chain i.e.
RequestIdInterceptor
and
ElapsedTimeInterceptor
. This adds both reusability, and readability to our code.
The implementation
Once we designed the abstraction the implementation was pretty straight forward:
https://gist.github.com/x0a1b/f7c64c92cfc6eae7ba1c2931bbabb475#file-middleware-go
So under 20 lines of code (excluding comments), we were able to build a nice middleware library. It's almost barebones, but the coherent abstraction of these few lines is just amazing. It enables us to write some slick middleware chains, without any fuss. Hopefully these few lines will inspire your middleware experience to be delightful as well.
Like what you see? Come
join us.