Go 1.22 shipped on February 6, 2024 and, although the changelog looks modest from the outside, it fixes two frictions that have shaped the language experience for almost a decade: loop variable semantics and the poverty of the standard HTTP router. Add to that a mature toolchain system and a handful of targeted standard library improvements. No single novelty reinvents Go, but the net result is that idiomatic 2024 code is written with fewer traps and fewer external dependencies.
The End of the Loop-Capture Trap
The most important change is also the least showy. Until 1.21, variables declared in a for header shared their address across iterations, and any closure or goroutine spawned inside the loop captured the same variable rather than its value at that instant. The pattern is so well known that it shows up in nearly every introductory course as an example of what not to do, and it has caused a hard-to-estimate number of production bugs, especially in concurrent pipelines where the symptom is a repeated result rather than an explicit error. The canonical case is a loop walking a short list of integers that keeps appending closures to a slice: each closure captures the index variable, and when they are later executed, they all print the last value the variable held rather than the value it had when the closure was created.
From 1.22 onward, if go.mod declares go 1.22, that same code produces the expected sequence because the variable is redeclared each iteration. The proposal was exercised throughout the 1.21 cycle behind GOEXPERIMENT=loopvar, and the Go team published an audit of Google’s codebase confirming the change fixed real bugs and broke almost nothing. Backwards compatibility holds because the new behavior only activates with the module’s version directive; projects staying on go 1.21 keep the old semantics.
This is the kind of fix that justifies an entire release. It doesn’t change how new code is written, but it erases a whole class of latent bugs in older code and removes the mental asterisk you had to attach whenever a goroutine launched inside a for.
ServeMux Learns to Route
The second heavyweight change is net/http.ServeMux. The previous implementation only matched by path prefix, without distinguishing methods or supporting parameters, so any halfway serious API reached for gorilla/mux, chi, or gin almost by inertia. In 1.22 the standard multiplexer accepts the method as a pattern prefix, named segments in braces, and a {rest...} wildcard that captures the tail of the path.
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", getUser)
mux.HandleFunc("POST /users", createUser)
mux.HandleFunc("DELETE /users/{id}", deleteUser)
func getUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
// ...
}
There are well-documented precedence rules for resolving conflicts between patterns, and captured values are read through r.PathValue. For a small-to-medium REST service, this makes an external router unnecessary. Frameworks still bring value when there’s shared middleware, automatic validation, or OpenAPI generation, but the entry step for serving a clean API without dependencies has dropped noticeably.
Range Over Integers and Other Language Touches
for i := range 10 is the most visible piece of syntactic sugar: it lets you write a numeric loop without the classic i := 0; i < n; i++. It doesn’t change performance or deep expressiveness, but it does align the writing with the range style that already dominates the language. It’s the kind of change that feels natural in six months and is forgotten as ever-different in twelve.
Toolchain: Module Version and Compiler Version
Go 1.21 introduced the go and toolchain directives in go.mod, and 1.22 finishes tuning their behavior. go marks the minimum language version the module assumes, and toolchain indicates which specific compiler version should be used. If the declared toolchain isn’t installed, go downloads it transparently. For teams where machines with different installed versions coexist, or for CI that isn’t always up to date, this solves a logistical problem that was recently still handled with hand-written scripts or asdf.
The Periphery of the Standard Library
Three changes deserve attention beyond the headline items. math/rand/v2 debuts the ChaCha8 generator, a cleaner API (IntN, Uint64), and breaks compatibility deliberately while keeping math/rand intact; it’s the first package to inaugurate the /v2 versioning scheme inside the stdlib. log/slog, stabilized in 1.21, receives polish and consolidates as the natural replacement for log for anyone who wants structured logging without depending on zap or zerolog. And govulncheck keeps maturing as the official tool to cross-check dependencies and stdlib against the Go vulnerability database; it’s reasonable to wire it into CI now.
The runtime brings small improvements to the profiler, the collector, and the memory allocator. Official notes mention one-to-three-percent ranges depending on workload, which in practice is imperceptible except on services whose profile was already optimized to the edge.
What Can Break on Migration
The loop semantics change is the one that needs the most attention, though tests usually catch it. If some piece of code deliberately relied on the shared variable between iterations (typical in ad-hoc counters built with closures), it will need rewriting. ioutil is fully deprecated; migration to os and io is mechanical and gopls suggests it. There are adjustments in JSON unmarshalling strictness that can affect code relying on undocumented behavior.
For most repositories the update consists of installing the 1.22 toolchain, flipping the go directive in go.mod, and running go vet ./... and the tests. An afternoon is enough outside pathological cases.
What Actually Changes and What Doesn’t
The loop-capture fix genuinely changes how concurrent Go is written: the defensive x := x on entering a goroutine inside a loop disappears. The ServeMux with methods and parameters changes the decision of which router to use on new projects; dropping a third-party router dependency is now a reasonable call, not a sporting restriction. Managed toolchains change how someone new is onboarded onto a project.
Range over integers, math/rand/v2, the runtime improvements, and the slog refinements are much more niche: useful when they come up, forgettable the rest of the time. govulncheck sits in the middle: it doesn’t change how code is written, but it does change how CI is integrated.
Go 1.22 confirms the language’s deep thesis: progress arrives slowly, goes through long proofing processes, and respects backwards compatibility almost religiously. In exchange for that slowness, when a change lands it tends to be solid and to have lived alongside real code for months. Updating doesn’t hand out spectacular performance or dazzling syntax, but it does pull two stones out of the shoe that had been there far too long.