Go Module 12 – Iterators and range-over-func
Iterators in slices, maps, strings and reflect
Use the iterator functions of slices, maps, strings and bytes, iterate a map in a fixed order, split text lazily, and walk struct fields with reflect.
What you will learn
- Use slices.All, Values, Backward, Chunk, Collect and Sorted on slices and on the sequences they yield
- Iterate a map in a fixed order with maps.Keys and slices.Sorted, and merge maps without losing quantities
- Split text lazily with strings.Lines, SplitSeq and FieldsSeq, and predict what each yields
- Measure the allocations an iterator saves over a slice, and choose between iterating and collecting
- Walk a struct's fields and a type's methods with the reflect iterators of Go 1.26
Before you start
What you’ll learn · Before you start
What you will learn
- Use slices.All, Values, Backward, Chunk, Collect and Sorted on slices and on the sequences they yield
- Iterate a map in a fixed order with maps.Keys and slices.Sorted, and merge maps without losing quantities
- Split text lazily with strings.Lines, SplitSeq and FieldsSeq, and predict what each yields
- Measure the allocations an iterator saves over a slice, and choose between iterating and collecting
- Walk a struct's fields and a type's methods with the reflect iterators of Go 1.26
Before you start
On this page
Printing a map’s keys in order used to take a loop, an append and a sort. Since Go 1.23 it is one expression,
slices.Sorted(maps.Keys(stock)), and in tour.go below it printed [atta dal rice]. The standard library now hands out
iterators over slices, maps, the lines and fields of text and the fields of a struct, and takes them back into slices
and maps.
You will use the iterators in slices and maps, walk a map in a fixed order, split text without building a slice, and walk a struct with reflect. Each program below ran on Go 1.27.1, and the output under it is its own.
Each package names its iterators for what they walk
The iterators in slices walk a slice: All yields each index with its element, Values the elements alone, Backward both from the end, and Chunk sub-slices of up to n elements. Package maps has All, Keys and Values. Packages strings and bytes cut text up into lines, pieces between separators and words. Package reflect, since Go 1.26, walks the fields of a struct type and the methods of any type. Functions such as Collect and Sorted go the other way: they take a sequence and build a slice or a map.
| Package | Returns an iterator | Takes an iterator | Since |
|---|---|---|---|
| slices | All, Values, Backward, Chunk |
Collect, AppendSeq, Sorted, SortedFunc, SortedStableFunc |
Go 1.23 |
| maps | All, Keys, Values |
Collect, Insert |
Go 1.23 |
| strings and bytes | Lines, SplitSeq, SplitAfterSeq, FieldsSeq, FieldsFuncSeq |
none | Go 1.24 |
| reflect | Type.Fields, Type.Methods, Type.Ins, Type.Outs, Value.Fields, Value.Methods |
none | Go 1.26 |
tour.go uses one from each package:
// tour.go: one iterator each from slices, maps, strings and reflect.
package main
import (
"fmt"
"maps"
"reflect"
"slices"
"strings"
)
// Ticket is one booking.
type Ticket struct {
PNR string
Coach string
Paise int
}
func main() {
queue := []string{"Asha", "Ravi", "Meena"}
for i, name := range slices.Backward(queue) {
fmt.Print(i, " ", name, "; ")
}
fmt.Println()
stock := map[string]int{"rice": 40, "atta": 25, "dal": 12}
fmt.Println(slices.Sorted(maps.Keys(stock)))
for field := range strings.FieldsSeq("GET /orders 200") {
fmt.Printf("[%s]", field)
}
fmt.Println()
for f := range reflect.TypeFor[Ticket]().Fields() {
fmt.Print(f.Name, " ", f.Type, "; ")
}
fmt.Println()
} Output
2 Meena; 1 Ravi; 0 Asha; [atta dal rice] [GET][/orders][200] PNR string; Coach string; Paise int;
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run tour.go
Backward gave Meena first, with her index 2. Sorted put the three keys in order, FieldsSeq split a request line into three words, and reflect gave each field of Ticket with its type.
Checkpoint
Check: Which package gives which iterator
A map’s keys have no order until slices.Sorted gives them one
The maps documentation says the order of Keys is not specified and may differ from one call to the next. A program that prints keys straight from a map can therefore print them differently on every run. slices.Sorted collects the keys into a new slice and sorts it, so the order is the same every time. Follow the expression through its steps:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- A map has no order. stock holds three keys. Go does not specify the order in which a loop over a map visits them, and the order may change from one walk to the next.
- Keys is only a function. maps.Keys(stock) does no work yet. It returns an iterator that, when called, yields the keys in whatever order the map gives them.
- Sorted pulls every key. slices.Sorted calls the iterator and appends each key it yields to a new slice, perhaps rice, then atta, then dal.
- Then it sorts. With every key collected, Sorted sorts the slice in increasing order, so the order the map gave no longer matters.
- The same result every time. The expression returns [atta dal rice] on every run, and a loop over that slice prints the stock in alphabetical order, as stock.go did.
stock.go counts the orders that 1,000 walks over maps.Keys produce, then sorts, then merges a delivery:
// stock.go: maps.Keys has no order until slices.Sorted gives it one; maps.Insert overwrites.
package main
import (
"fmt"
"maps"
"slices"
)
func main() {
stock := map[string]int{"rice": 40, "atta": 25, "dal": 12}
// Collect the keys 1,000 times, in whatever order maps.Keys gives them, and count the different orders.
seen := map[string]bool{}
for range 1000 {
seen[fmt.Sprint(slices.Collect(maps.Keys(stock)))] = true
}
fmt.Println("more than one order in 1,000 walks:", len(seen) > 1)
keys := slices.Sorted(maps.Keys(stock))
fmt.Println("sorted keys:", keys)
for _, k := range keys {
fmt.Printf("%s=%d ", k, stock[k])
}
fmt.Println()
// A delivery of 10 kg of rice and 5 kg of salt, merged with maps.Insert.
delivery := map[string]int{"rice": 10, "salt": 5}
maps.Insert(stock, maps.All(delivery))
fmt.Println("after Insert: rice =", stock["rice"], "salt =", stock["salt"])
} Output
more than one order in 1,000 walks: true sorted keys: [atta dal rice] atta=25 dal=12 rice=40 after Insert: rice = 10 salt = 5
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run stock.go
The walks produced more than one order, and Sorted gave [atta dal rice] every time. The last line is a trap: maps.Insert writes every pair of the delivery into the map, so rice went from 40 to 10 instead of 50.
Merging quantities with maps.Insert
maps.Insert(stock, maps.All(delivery)) overwrites each value with the delivery’s. To add quantities, loop:
for k, n := range delivery { stock[k] += n }.
Checkpoint
Check: Map order, Sorted and Insert
strings and bytes split text one piece at a time
strings.SplitSeq(s, sep) yields the same pieces as strings.Split, without building the slice, and FieldsSeq does
the same for Fields. Lines yields each line with its newline, a last line without one as it is, and nothing at all for
an empty string. Package bytes has the same functions for byte slices, and its documentation and that of strings call
SplitSeq and Lines single-use iterator: An iterator that can walk its sequence only once, usually because it reads a stream such as an io.Reader. A second walk yields nothing or carries on where the first stopped, and its doc comment should say so. More in “Range-over-func iterators with iter.Seq”:
// split.go: strings.SplitSeq, Lines and FieldsSeq yield pieces of a string one at a time.
package main
import (
"fmt"
"strings"
)
func main() {
for col := range strings.SplitSeq("pnr,coach,fare", ",") {
fmt.Printf("[%s]", col)
}
fmt.Println()
log := "GET /orders 200\nPOST /orders 201\nGET /health 200"
for line := range strings.Lines(log) {
fmt.Printf("%q\n", line)
}
// Stop at the coach: the loop asks for 2 of the row's 6 fields, and the rest are never cut out.
row := "4213567890,S4,Asha,34,545,confirmed"
asked := 0
for field := range strings.SplitSeq(row, ",") {
asked++
if asked == 2 {
fmt.Println("coach:", field, "after", asked, "fields")
break
}
}
} Output
[pnr][coach][fare] "GET /orders 200\n" "POST /orders 201\n" "GET /health 200" coach: S4 after 2 fields
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run split.go
The header split into its three columns. Lines kept each newline, so %q shows \n on two of the three lines. The
last loop wanted the coach, the second field, and broke there, so SplitSeq never cut out the other 4: splitting this
way is lazy evaluation: Producing each value only when the consumer asks for it, so the work stops when the consumer stops. strings.SplitSeq is lazy: a loop that breaks after two pieces leaves the rest of the string unsplit. strings.Split is not..
Comparing lines from Lines
Trim the newline before you compare a line, with strings.TrimSuffix(line, "\n"), or the match fails on every line
but the last.
Checkpoint
Check: SplitSeq, FieldsSeq and Lines
What an iterator saves over a slice, measured in allocations and bytes Pro: opens with a Pro, Premium or Ultimate pass
An iterator saves memory only when nobody collects what it yields. This part counts the allocations and bytes of Split, Fields and Lines against their iterator forms, and of collecting a map’s keys in two ways, then turns the numbers into a rule for when to iterate and when to collect.
In this part:
- Split, Fields and Lines against their iterator forms, measured
- Collecting keys: Sorted against a slice sized once, measured at 4 and 10,000 keys
- Iterate or collect: a rule from the numbers
2 runnable examples (Go) · about 8 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Chunk aliasing, code points, the move from x/exp/maps and reflect's iterators Pro: opens with a Pro, Premium or Ultimate pass
Four details catch people who move to the standard library’s iterators. This part reproduces each one in a program that runs: a write through a chunk, an empty separator on Hindi text, an old build that stops compiling, and a struct walked with reflect.
In this part:
- Writes and appends through a chunk, and what each does to the original
- SplitSeq with an empty separator, piece by piece on Hindi text
- Moving code from golang.org/x/exp/maps to package maps, call by call
- Walking struct fields, methods and parameters with reflect’s iterators
4 runnable examples (Go) · about 7 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Practise: three helpers rewritten with iterators
practice/helpers.go has three helpers to write with this lesson’s functions, and practice/helpers_test.go tests them on stock, a booking row and fares:
Go (helpers.go) · practice/helpers.go
// Package practice holds three helpers to rewrite with the standard library's iterators.
package practice
// KeysInOrder returns the keys of m in increasing order. Use maps.Keys and slices.Sorted.
func KeysInOrder(m map[string]int) []string {
return nil // write it
}
// ThirdField returns the third comma-separated field of row, or "" when row has fewer than three. Use
// strings.SplitSeq and stop as soon as you have the field.
func ThirdField(row string) string {
return "" // write it
}
// BatchTotals adds up the values of xs in batches of n, from the left; the last batch may be shorter. Use
// slices.Chunk. It returns nil for an empty xs.
func BatchTotals(xs []int, n int) []int {
return nil // write it
} Go (helpers_test.go) · practice/helpers_test.go
package practice
import (
"fmt"
"testing"
)
func TestKeysInOrder(t *testing.T) {
got := KeysInOrder(map[string]int{"rice": 40, "atta": 25, "dal": 12})
if fmt.Sprint(got) != "[atta dal rice]" {
t.Errorf("KeysInOrder = %v, want [atta dal rice]", got)
}
}
func TestThirdField(t *testing.T) {
for _, c := range []struct{ row, want string }{
{"4213567890,S4,Asha,34", "Asha"},
{"4213567890,S4", ""},
{",,", ""},
} {
if got := ThirdField(c.row); got != c.want {
t.Errorf("ThirdField(%q) = %q, want %q", c.row, got, c.want)
}
}
}
func TestBatchTotals(t *testing.T) {
got := BatchTotals([]int{545, 1450, 2050, 3420, 980}, 2)
if fmt.Sprint(got) != "[1995 5470 980]" {
t.Errorf("BatchTotals(…, 2) = %v, want [1995 5470 980]", got)
}
if got := BatchTotals(nil, 2); got != nil {
t.Errorf("BatchTotals(nil, 2) = %v, want nil", got)
}
} The starter code compiles, and every test fails:
from session import run
# The starter code of practice/ compiles, and every test fails until the three helpers are written.
run(
"""
cd practice && go test .
""",
expect_failure=["cd practice && go test ."],
) Output
$ cd practice && go test .
--- FAIL: TestKeysInOrder (0.00s)
helpers_test.go:11: KeysInOrder = [], want [atta dal rice]
--- FAIL: TestThirdField (0.00s)
helpers_test.go:22: ThirdField("4213567890,S4,Asha,34") = "", want "Asha"
--- FAIL: TestBatchTotals (0.00s)
helpers_test.go:30: BatchTotals(…, 2) = [], want [1995 5470 980]
FAIL
FAIL example.com/practice 1.371s
FAIL
[exit status 1]
This output changes from run to run: go test prints how long each test and the whole run took, which differs from one run to the next.
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 practice_session.py
Make go test . pass in the practice folder. A first hint: BatchTotals ranges over slices.Chunk(xs, n), adds up
each chunk and appends the total to its result.
The questions below cover the first three sections, and each answer is explained as soon as you give it.
Practice that adapts to you
Practise this lesson
9 questions about 7 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher): What does slices.Sorted(maps.Keys(m)) return, and why use it to print a map’s keys?
It returns a new slice of the map’s keys in increasing order. A map has no order of its own: Go does not specify the order of a loop over a map or of maps.Keys, and it can change between walks. Sorting the collected keys gives the same output on every run.
Three longer questions of the module bank build on this lesson:
-
Mid to senior
A log parser calls strings.Split on 50,000 lines a second to read the status code, the third of nine fields, and profiles show heavy allocation and garbage collection. What would you change, what would you measure before and after, and when would you keep Split?
-
Mid to senior
A batch uploader splits orders with slices.Chunk(orders, 100), appends a trailer record to each batch and rounds every amount in a batch before sending. Afterwards some orders in the caller's slice have changed. Which operation changed them, why, and how do you make the uploader safe?
-
Mid to staff
Your team replaces golang.org/x/exp/maps with the standard maps package across a large codebase, and dozens of builds break at lines like keys := maps.Keys(m); sort.Strings(keys). Explain the failure, give the fix for each kind of call site, and say how you would roll the change out.
Key takeaways
- The iterators in slices walk a slice forwards, backwards or in chunks; Collect, AppendSeq and Sorted turn a sequence back into a slice.
- A map has no order, so sorted output takes slices.Sorted over maps.Keys; maps.Insert overwrites values, it never adds them.
- SplitSeq, FieldsSeq and Lines yield the pieces of a string one at a time, Lines with each newline kept, and a loop that stops early stops the splitting.
Terms in this lesson
- lazy evaluation
- Producing each value only when the consumer asks for it, so the work stops when the consumer stops. strings.SplitSeq is lazy: a loop that breaks after two pieces leaves the rest of the string unsplit. strings.Split is not.
References
- Package slices (The Go Authors)
- Package maps (The Go Authors)
- Package strings: Lines (The Go Authors)
- Package strings: SplitSeq (The Go Authors)
- Package bytes (The Go Authors)
- Package reflect (The Go Authors)
- Package testing: Benchmark (The Go Authors)
- Package golang.org/x/exp/maps (The Go Authors)
- Go 1.23 Release Notes (The Go Authors)
- Go 1.24 Release Notes (The Go Authors)
- Unicode Standard Annex #29: Unicode Text Segmentation (The Unicode Consortium)
- Go 1.26 Release Notes (The Go Authors)
Report a problem with this lesson
Kept only in this browser. Your Learn progress