Skip to Content
Course content

136: http.ServeMux and Routing

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I remember a developer I mentored a few years back who was building a small internal API for a logistics company. He was using http.HandleFunc everywhere. At first, it seemed like the perfect shortcut, but as his project grew, he started writing integration tests. He quickly hit a wall: because http.HandleFunc registers routes to the DefaultServeMux—which is just a global variable under the hood—his tests started panicking. He'd run one test, the route would be registered, and the second test would crash because the route was already defined. He spent a whole afternoon fighting the global state before I showed him how to instantiate his own multiplexer.

Breaking Away from the Global Mux

In Go, a ServeMux is essentially an HTTP request multiplexer. It matches the URL of an incoming request against a list of registered patterns and calls the handler for the pattern that most closely matches. While the DefaultServeMux is convenient for a quick prototype, you almost never want to use it in a production application. Using a global router makes your code harder to test, creates potential security risks if third-party packages register routes you don't know about, and prevents you from running multiple server instances with different configurations in the same process.

Instead, you should create your own mux using http.NewServeMux(). This gives you a local object that you control entirely. Here is how you'd actually structure a small routing system for a book inventory service:

mux := http.NewServeMux()

mux.HandleFunc("/books", listBooksHandler)
mux.HandleFunc("/health", healthCheckHandler)

// Now we pass our custom mux into the server
err := http.ListenAndServe(":8080", mux)
if err != nil {
    log.Fatal(err)
}

By doing this, you've isolated your routing logic. If you need to spin up a second server on a different port for an admin panel, you just create a second ServeMux and you're good to go. No collisions, no global state headaches.

The Nuance of Trailing Slashes

One of the most common "gotchas" I see with ServeMux is how it handles trailing slashes. It's not just a stylistic choice; it changes how Go matches the URL. If you register a pattern like /books (no trailing slash), it matches exactly that path. If a user visits /books/, they'll get a 404.

However, if you register /books/ (with the trailing slash), it becomes a subtree match. This means it will match /books/, /books/123, /books/edit/456, and anything else that starts with that prefix. I usually recommend using the trailing slash for resources that have sub-paths and the exact match for singular actions or endpoints.

mux := http.NewServeMux()

// This ONLY matches "/api/status"
mux.HandleFunc("/api/status", statusHandler)

// This matches "/api/users", "/api/users/1", "/api/users/search", etc.
mux.HandleFunc("/api/users/", usersHandler)

Wiring the Mux to the Server

The http.ListenAndServe function takes two arguments: the network address and a Handler. Since http.ServeMux implements the Handler interface (it has a ServeHTTP method), you can pass it directly as the second argument. If you pass nil there, Go defaults back to the DefaultServeMux, which brings us right back to the problem our logistics developer had. Always pass your explicit mux instance to ensure your server remains predictable and testable.




📋 Practical Task

Exercise: Build a Multi-Zone API Router

Your task is to create a Go program that implements a custom ServeMux to handle three different routing scenarios for a simulated "Cloud Dashboard" API. You must avoid using http.HandleFunc (the global version) and instead use a locally instantiated mux.

  • Exact Match: Create a route for /ping that returns the text "pong".
  • Subtree Match: Create a route for /metrics/ that handles any request starting with that prefix (e.g., /metrics/cpu or /metrics/memory) and returns the text "Displaying system metrics".
  • Exact Match: Create a route for /version that returns the current version of your API (e.g., "v1.0.0").

Ensure your server starts on port 8080 and passes your custom mux into http.ListenAndServe. Test your implementation by visiting /ping, /metrics/cpu, and /version in your browser or via curl.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.