Gorilla vs Pat vs Routes:
A Mux Showdown

2nd September 2013

One of the first things I missed when learning Go was being able to route HTTP requests to handlers based on the pattern of a URL path, like you can with web frameworks like Sinatra and Django.

Although Go's ServeMux does a great job at routing incoming requests, it only works for fixed URL paths. To support pretty URLs with variable parameters we either need to roll a custom router (or HTTP request multiplexer in Go terminology), or look to a third-party package.

In this post we'll compare and contrast three popular packages for the job: Pat, Routes and Gorilla Mux. If you're already familiar with them, you might want to skip to the benchmarks and summary.

Pat

Pat by Blake Mizerany is the simplest and lightest of the three packages. It supports basic pattern matching on request paths, matching on request method (GET, POST etc), and the capture of named parameters.

The syntax for defining URL patterns should feel familiar if you're from the Ruby world – named parameters start with a colon, with the remainder of the path matched literally. For example, the pattern /user/:name/profile would match a request to /user/oomi/profile, with the name oomi captured as a parameter.

It's worth pointing out that behind the scenes Pat uses a custom algorithm for pattern matching, rather than a regular expression based approach like the other two packages. In theory this means it should be more a little more optimised for the task at hand.

Let's take a look at a sample application using Pat:

$ mkdir pat-example && cd pat-example
$ touch app.go
$ go get github.com/bmizerany/pat
File: app.go
package main

import (
  "github.com/bmizerany/pat"
  "log"
  "net/http"
)

func main() {
  mux := pat.New()
  mux.Get("/user/:name/profile", http.HandlerFunc(profile))

  http.Handle("/", mux)

  log.Println("Listening...")
  http.ListenAndServe(":3000", nil)
}

func profile(w http.ResponseWriter, r *http.Request) {
  params := r.URL.Query()
  name := params.Get(":name")
  w.Write([]byte("Hello " + name))
}

We'll quickly step through the interesting bits.

In the main function we start by creating a new HTTP request multiplexer (or mux for short) with Pat. Then we add a rule to the mux so that all GET requests which match the specified pattern are routed to the profile function.

Next we use the Handle function to register our custom mux as the handler for all incoming requests in Go's DefaultServeMux.

Because we're only using a single handler in this code, an alternative approach would be to skip registering with the DefaultServeMux, and pass our custom mux directly to ListenAndServe as the handler instead.

When a request gets matched, Pat adds any named parameters to the URL RawQuery. In the profile function we then access these in the same way as a normal query string value.

Go ahead and run the application:

$ go run app
Listening...

And visit localhost:3000/user/oomi/profile in your browser. You should see a Hello oomi response.

Pat also provides a couple of other nice touches, including redirecting paths with trailing slashes. Here's the full documentation.

Routes

Routes by Drone provides a similar interface to Pat, with the additional benefit that patterns can be more tightly controlled with optional Regular Expressions. For example, the two patterns below are both valid, with the second one matching if the name parameter contains lowercase letters only:

Routes also provides a few other nice features, including:

Basic usage of Routes is almost identical to Pat:

$ mkdir routes-example && cd routes-example
$ touch app.go
$ go get github.com/drone/routes
File: app.go
package main

import (
  "github.com/drone/routes"
  "log"
  "net/http"
)

func main() {
  mux := routes.New()
  mux.Get("/user/:name([a-z]+)/profile", profile)

  http.Handle("/", mux)

  log.Println("Listening...")
  http.ListenAndServe(":3000", nil)
}

func profile(w http.ResponseWriter, r *http.Request) {
  params := r.URL.Query()
  name := params.Get(":name")
  w.Write([]byte("Hello " + name))
}

Gorilla Mux

Gorilla Mux is the most full-featured of the three packages. It supports:

Additionally the matchers can be chained together, giving a lot of potential for granular routing rules if you need them.

The pattern syntax that Gorilla uses is slightly different to the other packages, with named parameters surrounded by curly braces. For example: /user/{name}/profile and /user/{name:[a-z]+}/profile.

Let's take a look at an example:

$ mkdir gorilla-example && cd gorilla-example
$ touch app.go
$ go get github.com/gorilla/mux
File: app.go
package main

import (
  "github.com/gorilla/mux"
  "log"
  "net/http"
)

func main() {
  rtr := mux.NewRouter()
  rtr.HandleFunc("/user/{name:[a-z]+}/profile", profile).Methods("GET")

  http.Handle("/", rtr)

  log.Println("Listening...")
  http.ListenAndServe(":3000", nil)
}

func profile(w http.ResponseWriter, r *http.Request) {
  params := mux.Vars(r)
  name := params["name"]
  w.Write([]byte("Hello " + name))
}

Fundamentally there's the same thing going on here as in the previous two examples. So although the syntax looks a bit different I won't dwell on it – the Gorilla documentation does a fine job of explaining it if it's not immediately clear.

Relative Performance

I ran two different sets of benchmarks on the packages. The first was a stripped-down benchmark to look at their performance in isolation, and the second was an attempt at profiling a more real-world use case.

In both tests I measured the number of successful requests across a ten second period, and took the average over 50 iterations, all running on my local machine.

For the 'stripped-down' benchmark, requests were simply routed to a handler that returned a 200 status code and message. Here are the code samples and results:

In this test the best performing package appeared to be Pat by a large margin. It handled around 30% more requests than Routes and Gorilla Mux, which were very evenly matched.

In the second benchmark requests were routed to a handler which accessed a named parameter from the URL, and then merged it with a HTML template read from disk. Here are the code samples and results:

In this benchmark the performance difference between the three packages was negligible.

Although it's always dangerous to draw conclusions from just one set of tests, it does point toward the overall performance impact of a router being much smaller for higher-latency applications, such as those with a lot of file system or database access in the handlers.

Summary

Pat would appear to be a good choice for scenarios where performance is important, you have a low-latency application, and only require simple pattern-based routing.

If you're likely to be validating a lot of parameter input with regular expressions in your application, then it probably makes sense to skip Pat and use Routes or Gorilla Mux instead, with the expressions built into your routing patterns.

For higher-latency applications, where there appears to be less of an overall impact due to router performance, Gorilla Mux may be a wise choice because of the sheer number of options and the flexibility it provides. Although I haven't looked at it in detail, larger applications with a lot of URL endpoints may also get a performance benefit from using Gorilla's nested routing too.

If you found this post useful, you might like to subscribe to my RSS feed.

Filed under: golang

‚Äč