Skip to content

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.

Published 1 min readRead in فارسی

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:

go
for _, order := range orders {
	go func() {
		process(order) // safe since Go 1.22 — no more `order := order`
	}()
}

Range over integers

go
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:

go
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:

go
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.Loop makes benchmarks harder to get wrong.
  • The omitzero JSON tag option omits zero values — including zero time.Time — which omitempty never did.
  • Maps got a new Swiss-table implementation: faster with no code change.
go
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:

go
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.