Go
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions and Methods
-
Section 4: Concurrency
-
Section 5: Packages and Tooling
-
Section 6: More Standard Library
-
Section 7: Building Services
-
Section 8: Advanced Go
-
Section 9: Go in the Cloud-Native Ecosystem
-
Section 10: Data Structures and Algorithms in Go
-
Section 11: Testing and Deployment
-
Section 12: Practical Projects
-
Section 13: More Standard Library Practice
-
Section 14: More Practice Projects
-
Section 15: Design Patterns in Go
-
Section 16: Interview Practice
-
Section 17: Package fmt In Depth
-
Section 18: Package strings and strconv
-
Section 19: Package os and io
-
Section 20: Package time
-
Section 21: Package sort and container
-
Section 22: Package encoding
-
Section 23: Package net/http In Depth
-
Section 24: Package context
-
Section 25: Package regexp and bytes
-
Section 26: Package errors In Depth
-
Section 27: Package crypto and hash
-
Section 28: Package flag and log
-
Section 29: Package sync In Depth
-
Section 30: More Practice Exercises
-
Section 31: Go Modules and Workspaces In Depth
-
Section 32: Generics Deep Dive (Go 1.18+)
-
Section 33: Testing Package In Depth
-
Section 34: More Interview and Whiteboard Practice
-
Section 35: Package math and unicode
-
Section 36: Package path and filepath
-
Section 37: Package database/sql
-
Section 38: Package text/template and html/template
-
Section 39: Package archive and compress
-
Section 40: Lower-Level net Package
-
Section 41: Package runtime and reflect
-
Section 42: Package embed and io/fs
-
Section 43: Go Assembly and CGO Basics
-
Section 44: Building CLIs and TUIs
-
Section 45: Go Performance Tuning
-
Section 46: More Real-World Projects
-
Section 47: Go in Production
-
Section 48: Go Design Patterns
-
Section 49: Go Interfaces Deep Dive
-
Section 50: Final Practice and Review
136: http.ServeMux and Routing
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
/pingthat returns the text "pong". - Subtree Match: Create a route for
/metrics/that handles any request starting with that prefix (e.g.,/metrics/cpuor/metrics/memory) and returns the text "Displaying system metrics". - Exact Match: Create a route for
/versionthat 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.
There are no comments for now.