Not sure how to structure your Go web application?

My new book guides you through the start-to-finish build of a real world web application in Go — covering topics like how to structure your code, manage dependencies, create dynamic database-driven pages, and how to authenticate and authorize users securely.

Take a look!

Which Go router should I use? (with flowchart)

Last updated:

When you start to build web applications with Go, one of the first questions you'll probably ask is "which router should I use?".

It's not an easy question to answer, either. There are probably more than 100 different routers available, all with different APIs, features, and behaviors. So for this blog post I've evaluated 30 popular ones, and created a shortlist of the best options along with a flowchart that you can use to help guide your choice.

Before we start, a few notes on terminology:

  • By supports method-based routing I mean that the router makes it easy to dispatch a HTTP request to different handlers based on the request method ("GET", "POST", etc).
  • By supports variables in URL paths I mean that the router makes it easy to declare routes like /movies/{id} where {id} is a dynamic value in the URL path.
  • By supports regexp route patterns I mean that the router makes it easy to declare routes like /movies/{[a-z-]+} where [a-z-]+ is a required regexp match in the URL path.
  • By supports host-based routes I mean that the router makes it easy to dispatch a HTTP request to different handlers based on the URL host (like rather than just the URL path.
  • By supports custom routing rules I mean that the router makes it easy to add custom rules for routing requests (such as routing to different handlers based on IP address, or the value in an Authorization header).
  • By conflicting routes I mean when you register two (or more) route patterns that potentially match the same request URL path. For example, if you register the routes /blog/{slug} and /blog/new then a HTTP request with the path /blog/new matches both these routes.

Shortlisted routers

There are four different routers that make the shortlist. They are http.ServeMux, julienschmidt/httprouter, go-chi/chi and gorilla/mux. All four are well-tested, well-documented, and actively maintained. They (mostly) have stable APIs, and are compatible with http.Handler, http.HandlerFunc, and the standard middleware pattern.

In terms of speed, all four routers are fast enough for (almost) every application and I recommend choosing between them based on the specific features that you need rather than performance. I've personally used all four in production applications at different times and have been happy with them.


I'll start by saying that if you can use http.ServeMux, you probably should.

As part of the Go standard library, it's very battle tested and well documented. Using it means that you don't need to import any third-party dependencies, and most other Go developers will also be familiar how it works. The Go 1 compatibility promise also means that you should be able to rely on http.ServeMux working the same way in the long-term. All of those things are big positives in terms of application maintenance.

Unlike most other routers it also supports host-based routes, incoming request URLs are automatically sanitized, and the way that it matches routes is smart too: longer route patterns always take precedence over shorter ones. This has the nice side-effect that you can register patterns in any order and it won't change how your application behaves.

The two main limitations of http.ServeMux are that it doesn't support method-based routing or variables in URL paths. But the lack of support for method-based routing isn't always a good reason to avoid it — it's quite easy to work around with some code like this:

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", index)

    err := http.ListenAndServe(":3000", mux)

func index(w http.ResponseWriter, r *http.Request) {
    if r.URL.Path != "/" {
        http.NotFound(w, r)

    // Common code for all requests can go here...

    switch r.Method {
    case http.MethodGet:
        // Handle the GET request...

    case http.MethodPost:
        // Handle the POST request...

    case http.MethodOptions:
        w.Header().Set("Allow", "GET, POST, OPTIONS")

        w.Header().Set("Allow", "GET, POST, OPTIONS")
        http.Error(w, "method not allowed", http.StatusMethodNotAllowed)

In those few lines you've effectively got method-based routing, along with custom 404 and 405 responses and support for OPTIONS requests. That's lot more than you get with many third-party routers.

Over time, I've come to realize that http.ServeMux has a lot of positives and it's perfectly sufficient in many cases. In fact, the only time I'd recommend not using it is when you need support for variables in URL paths or custom routing rules. In those cases, trying to work with http.ServeMux can get a bit hairy and I think it's generally better to opt for a third-party router.


I think that julienschmidt/httprouter is about as close to 'perfect' as any third-party router gets in terms of its behavior and compliance with the HTTP specs. It supports method-based routing and dynamic URLs, automatically handles 404 and 405 responses correctly (including allowing you to set custom handlers for these responses), and also automatically handles OPTIONS requests for you too.

It doesn't support host-based routes, custom routing rules or regexp route patterns. It's also important to note that httprouter does not allow conflicting routes, like /post/create and /post/:id. That's objectively a good thing because it helps avoid bugs, but may be a problem if you need to use conflicting routes (e.g. to align with the routes used by an existing system).

One downside of httprouter is that the API and documentation is a bit confusing. The package was first published prior to the introduction of request context in Go 1.7, and lot of the current API still exists in order to support these older versions of Go. Nowadays, you can write your handlers using regular http.Handler and http.HandlerFunc signatures and all you need is the router.Handler() and router.HandlerFunc() methods to register them. For example:

func main() {
    router := httprouter.New()

    router.HandlerFunc("GET", "/", indexGet)
    router.HandlerFunc("POST", "/", indexPost)

    err := http.ListenAndServe(":3000", mux)

func indexGet(w http.ResponseWriter, r *http.Request) {
    // Handle the GET request...

func indexPost(w http.ResponseWriter, r *http.Request) {
    // Handle the POST request...


The go-chi/chi package supports method-based routing, variables in URL paths, and regexp route patterns. Like httprouter, it automatically handles 404 and 405 responses and allows you to set custom handlers for them.

But my favorite thing about chi is that you can create 'groups' of routes which use specific middleware, like in the code snippet below. This is really useful in larger applications where you have lots of middleware and routes to manage.

r := chi.NewRouter()

// Middleware used on all routes

r.Get("/one", exampleHandlerOne)

r.Group(func(r chi.Router) {
    // Middleware only used in this route group

    r.Get("/two", exampleHandlerTwo)

It's important to note that chi does allow conflicting routes, with routes being matched in the order that they are declared.

Two downsides of chi are that it doesn't automatically handle OPTIONS requests, and an Allow header won't be set if you are using a custom handler for 405 responses. If you're building a web application or a private API, then those things probably aren't a big problem — but if you're building a public API it's something to be aware of. Like httprouter, it also doesn't support host-based routes.

As a note of caution, there have been 5 major version increments for chi in the past 6 years — most of which include breaking changes. History isn't necessarily a predictor of the future, but does suggest that backward-compatibility and not making breaking changes is less of a priority for chi than some of the other routers on this shortlist. In contrast httprouter and gorilla/mux haven't made any breaking changes in this time.


The gorilla/mux package is perhaps the most famous Go router, and with good reason. It's packed full of features, including support for method-based routing, dynamic URLs, regexp route patterns, and host-based routing. Importantly, it's the only router on this shortlist which supports custom routing rules and route 'reversing' (like you get in Django, Rails or Laravel). It also allows you to set custom handlers for 404 and 405 responses. Another nicety of Gorilla mux is that you can match multiple HTTP methods to the same handler in a single line of code (all the other routers require you to create individual routes for each method).

It's downsides are basically the same as chi — it doesn't automatically handle OPTIONS requests like httprouter does, and it doesn't automatically include an Allow header in all 405 responses. Again, like chi, it does allow conflicting routes, with routes being matched in the order that they are declared.

Given that the downsides of chi and gorilla/mux are similar, picking between the two is fairly straightforward: opt for gorilla/mux if you need support for custom routing rules, host-based routing or route 'reversing'. If you don't need those 'advanced' features then it's probably better to go with chi because of the nice features you get for managing middleware, especially if you're building a large application.

Honorable mentions

Two other routers that I think are worth mentioning are bmizerany/pat and matryer/way. I have a soft spot for both of these because they are deliberately simple. They have small APIs and very clear and understandable code, which makes it easy to grasp exactly how the router is working behind the scenes. The code behind matryer/way in particular is well worth a read.

While they're less featureful than the other routers in the shortlist, I think that their simplicity makes them a good fit for use in tutorials (especially tutorials aimed at new gophers), or as a starting point/inspiration if you fancy building your own custom router.

If you like small and simple routers, you might also want to consider alexedwards/flow. I wrote this a couple of years ago to brings together my favorite features from all the routers above, and there's a short introductory post about it here.


With the various pros and cons and supported features in mind, this flowchart should help you choose between the four shortlisted routers.

Other routers

The other routers that I evaluated are listed below, along with a short note to explain why they didn't made the shortlist.

Repository Notes
celrenheit/lion Currently unmaintained.
claygod/Bxog Currently unmaintained.
clevergo/clevergo Uses custom handler signature (not http.Handler or http.HandlerFunc).
dimfeld/httptreemux Doesn’t fully support http.Handler. Requires middleware for setting custom 404/405 handlers.
donutloop/mux Currently unmaintained.
gernest/alien Currently unmaintained.
go-ozzo/ozzo-routing Uses custom handler signature (not http.Handler or http.HandlerFunc).
go-playground/lars Currently unmaintained.
go-zoo/bone Good, but has similar use case to chi (which offers more). Incomplete tests.
go101/tinyrouter Verbose route declarations. Doesn’t automatically send 405 responses.
gocraft/web Currently unmaintained.
goji/goji Slightly unusual, but flexible, API which supports custom matchers. Requires middleware for setting custom 404/405 handlers. Good, but I think gorilla/mux offers similar features and is easier to use.
goroute/route Uses custom handler signature (not http.Handler or http.HandlerFunc).
gowww/router Good, but has similar use case to chi (which offers more). No way to set custom 405 handler.
GuilhermeCaruso/bellt No way to set custom 404 or 405 handlers.
husobee/vestigo Currently unmaintained. Only supports http.HandlerFunc.
naoina/denco Currently unmaintained.
nbari/violetear Good, but has similar use case to chi (which offers more). Wraps http.ResponseWriter with own custom type, which may cause problems in some cases.
nbio/hitch Lacking documentation.
nissy/bon Currently unmaintained.
razonyang/fastrouter Currently unmaintained.
rs/xmux Currently unmaintained. Uses custom handler signature (not http.Handler or http.HandlerFunc).
takama/router Currently unmaintained.
vardius/gorouter Good, but has similar use case to chi (which offers more). Four major versions in 5 years suggests the API may not be reliable.
VividCortex/siesta Good, but has similar use case to chi (which offers more). No way to set custom 405 handler.
xujiajun/gorouter Currently unmaintained.