44.4 Go 1.22: Enhanced ServeMux, Loop Variable Semantics Fix, math/rand/v2
Alright, let’s talk about Go 1.22. This isn’t one of those earth-shattering, “rewrite your entire worldview” releases. It’s better. It’s a collection of thoughtful, pragmatic improvements that fix actual, daily annoyances. It’s the language designers listening to years of collective grumbling from the trenches and doing something about it. Let’s dive into the three headliners.
The ServeMux Finally Grew Up
For years, Go’s built-in http.ServeMux has been… fine. It was the reliable, slightly dull Toyota Corolla of HTTP routers: it got you from GET / to your handler function without fuss, but it lacked the features you’d find in every third-party router built in the last decade. Well, in 1.22, it finally got a turbocharger and a GPS.
The two biggest limitations were the inability to match HTTP methods (GET, POST, etc.) and to handle dynamic URL segments (like /users/123). You’d have to parse that 123 out yourself inside the handler. Not anymore.
// Go 1.22: The new, enhanced ServeMux
mux := http.NewServeMux()
// Method matching: FINALLY. This handler *only* responds to POST requests.
mux.Handle("POST /users/create", http.HandlerFunc(createUserHandler))
// Named path variables: This is the game-changer.
// The pattern `/users/{id}` matches `/users/123`, `/users/foo`, etc.
// The value is available via `request.PathValue("id")`.
mux.Handle("GET /users/{id}", http.HandlerFunc(getUserHandler))
// You can even do some wild stuff with wildcards.
// This matches any path under `/static/`.
mux.Handle("GET /static/{*path}", http.HandlerFunc(staticFileHandler))
func getUserHandler(w http.ResponseWriter, r *http.Request) {
// No more r.URL.Path parsing! It's just... here.
userID := r.PathValue("id")
fmt.Fprintf(w, "Fetching user with ID: %s", userID)
}
Why is this a big deal? It means for a huge class of simple APIs and applications, you can now drop your external router dependency (gorilla/mux, chi, httprouter, etc.). The standard library is now good enough. It reduces your project’s external dependencies and keeps things simple. The pattern matching is intentionally not as complex as some third-party options—no regular expressions in the routes—and that’s a feature. It keeps things fast, predictable, and secure.
Loop Variables: The Great Schism is Over
This is the change that broke the internet for Go programmers. Or at least caused a lot of very heated Twitter threads. For over a decade, this has been the most common “gotcha” for newcomers and a frequent bug for the rest of us.
In every Go version before 1.22, loop variables (i, v) were created once per loop and reused in each iteration. This meant when you launched a goroutine or created a closure inside a loop capturing that variable, you were almost always capturing a reference to the single variable, not the value it had at that specific iteration. The result? All your goroutines would see the final value of the variable after the loop finished.
// The Old and Busted Way (Pre-1.22)
for _, url := range []string{"a.com", "b.com", "c.com"} {
go func() {
// This is a classic bug. All three goroutines will likely
// print "c.com", because they all share the same `url` variable.
fmt.Println(url)
}()
}
// The classic fix was to pass it as a parameter to the closure:
go func(u string) {
fmt.Println(u) // This works correctly.
}(url)
It was absurd. We all just accepted this weird mental overhead and wrote url := url a thousand times. Well, no more. In Go 1.22, the language semantics have changed. Each iteration of the loop now creates new variables.
The code above, the buggy version, now Just Works™. It does what you intuitively thought it would do in the first place. This is a breaking change, but the Go team rightly classified it as a “language fix.” The old behavior was essentially a bug in the spec. To maintain compatibility, the change only applies to modules with a go.mod file declaring go 1.22 or later. If your module is set to go 1.21, you get the old, annoying behavior. This is a brilliant move—it prevents existing code from breaking unexpectedly.
math/rand/v2: A Lesson in Honest Naming
The original math/rand has some, uh, historical baggage. The global generator, accessed by functions like rand.Intn(), is mutex-protected for safety. That’s fine, but it’s a performance bottleneck if you’re generating a lot of numbers from multiple goroutines. The Read method was also notoriously slow.
Enter math/rand/v2. The v2 in the name is an admission: “We wanted to fix the API, and we couldn’t do it without breaking things, so here’s a new package.” I respect the honesty.
What’s new?
- No more global generator. You must create a generator. This is a nudge towards better practice.
- Faster algorithms: It introduces a modern Permuted Congruential Generator (PCG) which is faster and has better statistical properties than the old default.
- New methods: Like
N, which generates a random number within a range in a way that’s less biased than the oldIntn()method for very large ranges. - A usable
Readmethod: It’s finally performant.
// Go 1.22: Using the new math/rand/v2
package main
import (
"fmt"
"math/rand/v2" // Note the /v2
)
func main() {
// You MUST create a generator now. The global one is gone.
r := rand.New(rand.NewPCG(742, 0)) // Seed it yourself
// New, better methods for ranges.
n := r.N(1000) // [0, 1000) - better for large ranges
fmt.Println(n)
// Or just use the classic IntN, which is still there.
fmt.Println(r.IntN(100))
}
The lesson here is clear: if you’re starting a new project, use v2. It’s faster and the API encourages better code. For existing code, math/rand isn’t going away, so you can migrate at your leisure. This is how you do a standard library upgrade without setting the world on fire.