Go 1.22 → 1.25: the changes I actually use
Loop variables that finally behave, range-over-int and iterators, a smarter ServeMux, WaitGroup.Go and container-aware GOMAXPROCS — the Go features that changed my day-to-day code.
Every Go release is small on purpose. But stack four of them together and the language I write today looks noticeably different from the one I wrote two years ago. These are the changes that actually show up in my production code — mostly in a Go trading backend.
1.22 — loop variables and a real router
Per-iteration loop variables
The classic bug is gone. Each iteration now gets its own copy of the loop variable, so closures and goroutines capture what you expect:
for _, order := range orders {
go func() {
process(order) // safe since Go 1.22 — no more `order := order`
}()
}Range over integers
for i := range 3 {
fmt.Println(i) // 0 1 2
}Method and wildcard patterns in net/http
For small services I no longer reach for a router library:
mux := http.NewServeMux()
mux.HandleFunc("GET /orders/{id}", getOrder)
mux.HandleFunc("POST /orders", createOrder)
func getOrder(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
// ...
}1.23 — iterators
Range-over-func turns "walk this collection" into a first-class, composable thing. The iter package defines the
shapes, and slices / maps gained helpers that return iterators:
func Filled(orders []Order) iter.Seq[Order] {
return func(yield func(Order) bool) {
for _, o := range orders {
if o.Status == StatusFilled && !yield(o) {
return
}
}
}
}
for o := range Filled(orders) {
settle(o)
}1.24 — quality-of-life
- Generic type aliases are fully supported.
testing.B.Loopmakes benchmarks harder to get wrong.- The
omitzeroJSON tag option omits zero values — including zerotime.Time— whichomitemptynever did. - Maps got a new Swiss-table implementation: faster with no code change.
type Order struct {
ID int64 `json:"id"`
FilledAt time.Time `json:"filledAt,omitzero"`
}1.25 — concurrency ergonomics and containers
sync.WaitGroup.Go removes the Add(1) / defer Done() dance:
var wg sync.WaitGroup
for _, feed := range feeds {
wg.Go(func() {
feed.Poll(ctx)
})
}
wg.Wait()And on Linux, the runtime now respects container CPU limits when choosing the default GOMAXPROCS — a real win
when the service runs in Docker with a CPU quota.
Takeaways
If your codebase targets Go ≥ 1.22, delete the x := x shadow copies, try ServeMux patterns before adding a
router, and reach for iterators when a function's job is "produce a sequence".
Always check the official release notes before upgrading — this post covers only what mattered to me.