Powermux — Yet another Go web router, for fun and profit

Andrew Burian
Axiom Zen Team
Published in
8 min readMay 9, 2018

In my mostly humble opinion, Go is one of the best languages I’ve ever developed a full scale program in. I’ve had lots of fun hacking in other languages, but when I sit down to write something serious or a hack that’s more than modestly complicated, I turn to Go first. I come from C, so I might be more than a little biased; I truly enjoy structs as objects and some of the patterns that Go encourages. In addition to the language fundamentals itself, I just love the Go standard library (for the most part).

Every standard library (stdlib) struggles with the balance of providing the functionality and standards that the language needs, while trying not to be all-dictating by forcing you into one scheme of doing things. I think Go nailed it, and I’m constantly impressed with the breadth of use that I can apply things to with the standard library.

Which is why Go’s web router makes me so sad. There are a few things I think are lacklustre in the stdlib. The web router, the logger, and a few other small nitpicks (which I won’t go into here).

Thegolang.org/x/libraries do a lot to fill in some of the smaller gaps, and I read all the release notes hoping some of my favourite tools would have reached standard. However, the web router seems destined to only ever be ‘okay’. To Go’s credit, I can’t imagine how hard it would be to decide what the “standard” way to do web routing would be. I think any attempt to impose more functionality to the stdlib router would spark an endless best practices and preferences debate, so some might argue their solution is for the best. At a basic level, it’s simple, it works, and it’s non-confrontational.

However, they leave a few things completely without standard or direction, meaning we suffer from a lack of cross-compatibility between all our favourite community developed libraries. The most glaring example is the lack of a standard middleware signature. The Go router takes no stance on middleware, and so there is no standard way to write a middleware function. And before the advent of the context.Context object was brought into the stdlib in Go-1.7, there wasn’t even a “correct” way to pass information down in a middleware even if you had one. So we’re left with a glut of libraries that introduce their own signatures, context implementations, declaration formats, etc. Each claims to be better, faster, and more versatile than the last, and none are compatible with each other.

My solution: Build another!

xkcd/927

Okay, to my own credit, I wrote another Go web router not in an attempt to create the one to rule them all, or fix all the problems the others had. I wrote a web router because I was curious, and at the time, I was up to my eyeballs in the HTTP spec. Learning the ins and outs of web traffic made me want to try my hand at doing it from scratch.

At the time, we were using a router that shall remain nameless, and in my opinion, it has one of the messiest context object implementations I’ve ever seen. Running it through a profiler sometimes showed dozens of calls to reflect, and dynamic type casts each request to make their system work, all of this at a huge performance penalty. It also had a bug in its handling of path parameters which had become a backwards-compatible guaranteed bug that seems unfixable. With what I didn’t like about this library in mind, and a look through the features of other routers I loved the most, I sat down with the kind of bright-eyed optimism only a developer about to make a huge mistake could have.

Enter: Powermux

Powermux is my answer to everything wrong in the web routers world. I’m also pleased to bits to report that after not much additional optimization or tweaks after my first write, it’s become our standard Go web router at Axiom Zen.

Powermux had a few core design principals in mind:

  • No external imports. I used the tried and true stdlib exclusively.
  • No strong workflow assumptions. I wanted to find the sweet spot where I could add more useful features without dictating how it must be used.
  • No custom context. Now that context was standard, no need to reinvent the wheel.
  • No Powermux specific signatures. This I had to fudge a bit. Technically, Powermux does declare a middleware signature, but other packages can use this signature without needing to import Powermux themselves, so almost a win.
  • Not too slow. I wasn’t going to hound microseconds in my first attempt, though I do intend to go back and optimize as much as possible. It just wasn’t allowed to be slower than our existing libraries.
  • Stdlib compatible. If you have a code base that uses the net/http.Router, I wanted Powermux to be a drop-in replacement so there was no need to rewrite what you already had.

Middlewares

The thing that bothered me the most about the stdlib was the lack of a middleware signature. I did my best to keep the signature Powermux expects as generic as possible and in the spirit of the stdlib.

type Middleware interface { 
ServeHTTPMiddleware(http.ResponseWriter, *http.Request, func(http.ResponseWriter, *http.Request))
}

The third argument, the function call for next item in the middleware/handler stack, was originally wrapped in a custom type powermux.NextMiddleware. I dropped it in favour of the more verbose and generic declaration so that no library providing middlewares would have to import Powermux to be compatible.

Middlewares have no custom context to use, no methods to manipulate routing — just the request, the response, and the next item in the callstack. It was as pure Go as I could make it. If the middleware needs to pass context down, it should use the req.Context and provided getter/setter functions to make it available to the handlers below.

Path Parameters

Another thing I think the web router can support is parameters as path components, as expected by REST. It was a subject of great internal debate as to whether or not I should support regex in these expressions, but I decided that the cleaner and simpler section matcher was powerful enough, made fewer assumptions, and was easier to set up.

Path parameters are defined in the url strings as :element like /users/:name/blame . Powermux then makes name available to the handler with powermux.PathParam("name", req) so a request to /users/andrew/blame gives you my name to your app to easily blame me for proliferating more web routers into the undeserving world.

Powermux also lets you define wildcard routes like /users/custom/* if you want to do matching on entire sub-trees.

If you define multiple routes with different kinds of matchers, Powermux preferentially chooses more precise matches first:

  • /users/andrew/blame Most precise, literal match.
  • /users/:name/blame Variable matches anything not a literal match.
  • /* Wildcard can catch anything else that doesn’t match the above.

A very non-standard declaration syntax

Coming from our previous router where everything was declared in huge unreadable nested blocks, I really wanted Powermux to be as modular as possibly in its declaration and setup. Internally, Powermux maintains its routing as a radix tree, and I wanted a declarative syntax to walk down the tree and have it behave the same everywhere. The Route function does exactly this.

Every call to Route walks a little further down the tree, creating it implicitly if it wasn’t there already. Each node can have handlers and middleware attached to it. When a request comes in, it too walks down this tree, passing through middlewares and, finally, a handler as it goes.

My particular work flow here, though definitely not the only one that can exist (and which my fellow engineers despise), is I tend to declare handler objects like UserHandler that are responsible for an entire branch of the tree off the root. Then the hander has a UserHandler.Setup function that takes a *powermux.Route and makes all its own decisions as to what paths and middleware are set on it. The main setup consists only of walking the first step down the tree mux.Route("/users") and providing the handler with that route. The handler doesn’t know what top level route feeds into it, and the top level router doesn’t know what the handler is doing below that point.

A complete example to illustrate this is available in the repo.

Like I said, even my fellow engineers at work don’t particularly agree with my workflow here, but it is in no way the prescribed way Powermux suggests you set up your routes. The goal was to have a declarative syntax that works nicely with any style and forces you into as few patterns as possible, which I think this achieves nicely.

So, was it worth it?

Yes. To me anyways. May the devs that have to deal with it have mercy on my soul.

It was extremely beneficial to me as a developer to have this sort of experience under my belt, and even though I did tell myself I wasn’t going to sweat micro-optimizations to start with, I did challenge myself to make sure I wasn’t making anything that would prevent me from optimizing later, or make any mistakes that would result in avoidable performance penalties.

It’s also helpful to have one of the most core pieces of our infrastructure code be something we control fairly directly. We know we won’t break things, we’ll be quick to patch discovered bugs, and we can optimize until our performance-hounding obsessive compulsive disorders are satisfied.

We’ve also started to build up a collection of common middlewares that can be used across all projects that all simply match the ServeHTTPMiddleware signature. Everything from logging, gzip, rate limiting, auth checks, anything that we need over and over again has gone into its own library, and is plugged into Powermux on demand.

There are still features to come, performance to tweak. I’m a little afraid of the fact that my GitHub handle has caps in it but after the logrus debacle I’m never changing that. All in all, I hope to continue to grow and evolve Powermux into the best darn router I can make it, even if it is just yet another Go web router.

Andrew is the political liaison between the dev team and a rack of heartless servers at Axiom Zen.

Try Powermux or leave feedback on GitHub.

--

--