Go's 1.22+ ServeMux vs Chi Router

Go 1.22 introduces a couple new features for Go. Among them are changes to the net/http package that make it easier to use the ServeMux. In a nutshell, the routing for this type is being updated to allow for HTTP methods and URL path values. (Note: That is a very short summary of what was changed, but will suffice for this article.)

For anyone unfamiliar with the ServeMux type, it is basically a router that allows developers to route HTTP requests to the correct HTTP handler. Prior to Go 1.22, it was fairly limited and would only route based on the request path. Routing based on HTTP methods, or really anything else, needed to be done using custom code or with a third party library (eg chi or gorilla/mux).

While this may sound bad, the reality wasn’t so bleak. A few third party libraries filled in all these gaps and became quite stable, so it was essentially a non-issue. Of all the libraries I might go get, my router was never one that concerned me or caused major issues.

For Go 1.22 a discussion was started around updating the net/http standard library. After a bit of back and forth about how it would work, how to prevent breaking changes, etc, the ServeMux type was then updated to support HTTP methods and URL path values. This makes it viable for most common routing use cases.

Since those changes have been merged into the master branch, I have had a few people reach out asking if I intend to use the standard library’s router instead of my previous preferences of chi or gorilla/mux. In some cases they are asking what I will use moving forward, and a few questions were regarding what I intend to use in my Web Development with Go course.

In this article I want to discuss the pros/cons of each option, and why I intend to continue using chi for most projects. Let’s start by looking at some of the differences between chi and the updated ServeMux.

The most obvious difference, at least at first glance, is how routes are declared with HTTP verbs (GET, POST, etc). Using Go’s updated ServeMux, the HTTP verb needs to be declared as part of the path string for a route.

mux := http.NewServeMux()
mux.HandleFunc("GET /signup", showSignupForm)
mux.HandleFunc("POST /signup", processSignupForm)

It isn’t a separate argument, and there aren’t helper methods like mux.Get(path). Presumably this is a single string argument to avoid introducing a breaking change to Go’s standard library, and helper methods are left out likely for simplicity. They very well may get added later, but I have no idea.

Chi, and most third party libraries, have methods for declaring routes with each HTTP verb.

r := chi.NewRouter()
r.Get("/signup", showSignupForm)
r.Post("/signup", processSignupForm)

While this is probably one of the first differences many people will notice, I am skeptical that this difference will matter. I suspect after a week of using the new ServeMux and I wouldn’t care.

In theory the ServeMux approach might lead to bugs where an HTTP verb is misspelled, but in practice I doubt this would matter. I am also fairly confident that tooling could catch these issues for any team that has concerns.

One of the less obvious differences is how URL path variables work. In a library like chi, there are a lot more options for how we can parse URL path variables including using regular expressions to define a path variable, and not being forced to use the entire expression between slashes as the URL path variable. A few quick examples:

Not every app will care about these details, and it would be pretty easy to redesign endpoints with these limitations. Where it will be problematic are applications that already have a set of paths that need to continue working; these will be harder to support with Go 1.22’s ServeMux and will require additional coding. Chances are projects with paths like this will never migrate away from their third party library.

Another notable difference are the helpers in libraries like Chi for declaring middleware and handling tasks like creating a sub-router. The following snippet demonstrates how to declare several routes nested under the /articles/:id path prefix, and all of these routes have the ArticleCtx middleware applied.

// Chi
r := chi.NewRouter()
r.Route("/articles/{articleID}", func(r chi.Router) {
  // Using specific middleware for these routes
  r.Use(ArticleCtx)

  // All of these paths have the /articles/:id/ prefix
  // GET /articles/123
  r.Get("/", getArticle)
  r.Put("/", updateArticle)
  r.Delete("/", deleteArticle)
  // POST /articles/123/publish
  r.Post("/publish", publishArticle)
})

With Go’s ServeMux, creating sub-routers is more awkward. It is possible without URL path values, but with them (as in this example) the ServeMux along wouldn’t really suffice. Developers would likely need to create their own helpers for created nested paths with a sub-router, and by that point I’d personally rather use Chi which I know is well tested and does what I need.

In this code we can also see how middleware helpers are provided by Chi. These are pretty minor, since middleware can be applied manually quite easily, but I have come to really appreciate the helpers in nested routes. They seem easier for new developers to get right, which matters a lot when working with junior developers.

Another factor here is using something old and reliable vs jumping on the hot new thing. Don’t get me wrong - I expect anything in Go’s standard library to be extremely well written, but Chi has been around quite some time. I know I can count on it to work and be reliable because I’ve used it for hundreds of thousands of web requests. Maybe I am getting old, but these days I like the tried and true approach when there isn’t a compelling reason to use something new.

This leads to the main question - What will I be using going forward?

At this point I don’t think this will shock anyone, but I plan to continue using Chi (or gorilla/mux) in projects that have these libraries installed. That might change if I discover a compelling reason to change, but until then I am content using the libraries that have always worked well for me. I suspect most projects would also benefit from taking this approach - “If it ain’t broke, don’t fix it”.

For new projects I’ll definitely try using the net/http package’s ServeMux. Worse case I need to swap it out for something like Chi, but historically this hasn’t been a painful refactor. I already use the pre-1.22 ServeMux in a few projects, so this isn’t really a change. What might change is how quickly I need to replace the ServeMux with Chi in a new project - in some cases this might never happen now.

The only project I am on the fence about is my course, Web Development with Go. On one hand, many people taking the course really prefer to use the standard library as much as possible. To accommodate this, the course uses Go’s database/sql package instead of any third party library for SQL queries, and I try to avoid third party libraries as much as is reasonable. By that logic, I should use the new ServeMux.

On the other hand, everything the course teaches about routing remains exactly the same whether we use Chi or ServeMux. We still need to learn about HTTP verbs, URL path variables, designing good endpoints for a web application, etc. I cannot imagine any scenario where changing libraries would make it easier for someone to learn how to route their application. I also utilize some of Chi’s helpers for nested routes, middleware, etc in the course, not to mention Chi is what I personally prefer for my production apps, so it feels a bit like lying to teach with a library that isn’t my preference. It feels like I am pandering to a vocal minority on Reddit rather than teaching what I truly feel is the best choice.

What I’ll probably do is leave the course as-is, but add lessons at the end of the course that show how to use the new ServeMux to achieve the same routing. This feels like the best of both worlds, and will be faster to add to the course when Go 1.22 releases. I’ll probably also turn this writeup into a video to help demonstrate how the libraries are different and similar in various ways.

That about sums it up. Nothing really major to be learned in this post, but hopefully it helps illustrate where Go’s new ServeMux will still differ from some of the third party routing libraries.

Learn Web Development with Go!

Sign up for my mailing list and I'll send you a FREE sample from my course - Web Development with Go. The sample includes 19 screencasts and the first few chapters from the book.

You will also receive emails from me about Go coding techniques, upcoming courses (including FREE ones), and course discounts.

Avatar of Jon Calhoun
Written by
Jon Calhoun

Jon Calhoun is a full stack web developer who teaches about Go, web development, algorithms, and anything programming. If you haven't already, you should totally check out his Go courses.

Previously, Jon worked at several statups including co-founding EasyPost, a shipping API used by several fortune 500 companies. Prior to that Jon worked at Google, competed at world finals in programming competitions, and has been programming since he was a child.

Related articles

Spread the word

Did you find this page helpful? Let others know about it!

Sharing helps me continue to create both free and premium Go resources.

Want to discuss the article?

See something that is wrong, think this article could be improved, or just want to say thanks? I'd love to hear what you have to say!

You can reach me via email or via twitter.

Recent Articles All Articles Mini-Series Progress Updates Tags About Me Go Courses

©2018 Jonathan Calhoun. All rights reserved.