TL;DR: What Is a Mux in Go? #
Go's mux is just a fancy word for an HTTP router, and you've probably already used one.
A mux takes incoming requests and matches them to the correct handler based on the request's path and method, like GET /api/users.
That’s it. Everything else in this post (modular routing, StripPrefix, slashes) is just how you use a mux well.
Clarifying Terms #
In Go, mux, when used in ServeMux, is short for a "multiplexer". If you come from a networking background, that term can feel overloaded (and advanced), so let's clarify what "multiplexing" means in different contexts:
- TCP multiplexing happens at the transport layer. It allows multiple logical connections (e.g., multiple processes or sessions) to share a single physical TCP connection, usually managed by a proxy or operating system.
- HTTP/2 multiplexing happens at the application layer. It allows multiple HTTP requests and responses to be interleaved over a single TCP connection: this is built into the HTTP/2 spec itself.
- Go multiplexing is not about concurrent streams or network efficiency.
At a 9,000-foot view, multiplexing just means taking many things and funneling them through one channel, then splitting them back out where they belong. The details differ by layer, but the core idea is routing or interleaving multiple inputs through a shared path.
Developers are notoriously bad at naming things: 'mux' is no exception.
How mux works in Go (beyond the TL;DR) #
In Go, when developers want to route different HTTP requests to different handlers, they use an *http.ServeMux{}: a multiplexer. A multiplexer is just a router: it takes incoming requests and sends them to the correct handler. For the most part, you can think of this as similar (but not identical) to React Router or Express Routing.
By default, when you call http.HandleFunc (or http.Handle), you're registering handlers to Go's global DefaultServeMux, which is implicitly used by http.ListenAndServe:
1// Inside net/http package:
2var DefaultServeMux = new(ServeMux)
So this code:
1// usersHandler is a typical http.HandlerFunc
2http.HandleFunc("/api/users", usersHandler)
Is short for:
1http.DefaultServeMux.HandleFunc("/api/users", usersHandler)
The power of initializing custom *http.ServeMux instances is that you can use them to route requests to different handlers without affecting the global DefaultServeMux. This can be useful for organizing your code or for testing purposes.
Overriding the Default Mux #
In the net/http package, http.ListenAndServe uses http.DefaultServeMux behind the scenes when nil is passed as the handler:
1// ListenAndServe listens on the TCP network address addr and then calls
2// [Serve] with handler to handle requests on incoming connections.
3// Accepted connections are configured to enable TCP keep-alives.
4//
5// The handler is typically nil, in which case [DefaultServeMux] is used.
6//
7// ListenAndServe always returns a non-nil error.
8func ListenAndServe(addr string, handler Handler) error {
9 server := &Server{Addr: addr, Handler: handler}
10 return server.ListenAndServe()
11}
Through this godoc, we can see that the handler is customizable with a custom ServeMux:
1func main() {
2 mux := http.NewServeMux()
3 mux.HandleFunc("/hello", helloHandler)
4
5 http.ListenAndServe(":8080", mux)
6}
This skips the global DefaultServeMux entirely and gives you full control over your routing. It's especially useful when you want to keep your routing self-contained or avoid polluting global state (e.g., for testing or multiple servers).
Basic Usage #
Let's say we want to create a simple API with the following endpoints:
1GET /api/users
2GET /api/users/{id}
3GET /api/posts
4GET /api/posts/{id}
As of the latest Go version, you can route these using the standard library mux:
1func main() {
2 http.HandleFunc("GET /api/users", usersHandler)
3 http.HandleFunc("GET /api/users/{id}", userHandler)
4 http.HandleFunc("GET /api/posts", postsHandler)
5 http.HandleFunc("GET /api/posts/{id}", postHandler)
6 // ...
7}
This default method works great for small projects, but as your API grows, you'll quickly run into organizational challenges. What happens when you have dozens of endpoints? How do you group related routes? How do you avoid repeating /api/ in every path?
In the next post, I'll explore Go's powerful routing patterns that let you build modular, scalable APIs using nothing but the standard library.