if/else, switch, the lone for loop, break, continue.
Modern range (Go 1.22 & 1.23)
Two recent Go releases quietly reshaped the most common loop in the language. If you've read older Go books or blog posts, the advice in this lesson supersedes what they tell you.
Go 1.22: the loop variable is now per-iteration
For years, this was the #1 Go newbie trap:
// Pre-1.22 — DON'T copy-paste this advice
funcs := []func(){}
for _, v := range []int{1, 2, 3} {
funcs = append(funcs, func() { fmt.Println(v) })
}
for _, f := range funcs { f() }
// Used to print 3, 3, 3 — every closure captured the SAME v.
Since Go 1.22, v is a fresh variable per iteration. The snippet above now
prints 1, 2, 3. The infamous v := v shadow workaround is no longer needed.
Go 1.22: range over an integer
You can now range straight over an integer count, no for i := 0; i < n; i++
ritual:
range 5 iterates i from 0 to 4. If you don't need the index, even shorter:
for range 3 {
fmt.Println("knock")
}
Go 1.23: range over a function (iterators)
Go 1.23 lets you range over a function — that's the foundation of the new
iter package. Library code increasingly returns
iterator functions you walk directly:
import (
"maps"
"slices"
)
m := map[string]int{"a": 1, "b": 2}
for k, v := range maps.All(m) { // maps.All returns an iter.Seq2
fmt.Println(k, v)
}
evens := slices.Collect(filter(nums, func(n int) bool { return n%2 == 0 }))
You can also write your own:
func upTo(n int) iter.Seq[int] {
return func(yield func(int) bool) {
for i := 0; i < n; i++ {
if !yield(i) { return } // consumer broke out of the loop
}
}
}
for v := range upTo(5) { fmt.Println(v) }
iter.Seq[V] is "single value per step"; iter.Seq2[K, V] is "two values per
step" (the shape maps.All returns).
What changed in your habits
- Stop writing
v := vinside range loops — it's now a no-op (and lint tools flag it). - Stop writing
for i := 0; i < n; i++when you just need a count — usefor i := range n. - Read
maps.All,slices.All, customiter.Seqreturns as ranges, not as channels or callbacks.