Go 1.23 made functions valid range targets. You can now iterate over anything — not just slices, maps, and channels.
Estimated Reading Time : 9m
What changed
Before Go 1.23, for range worked on a fixed set of types: arrays, slices, maps, strings, channels, and integers. If you had a custom data structure, you returned a slice and the caller ranged over that.
Go 1.23 adds iterator functions — functions that yield values one at a time to a for range loop. No intermediate slice, no channel goroutine, no special interface.
The iterator signatures
The iter package defines two function types:
// yields a single value per iteration
type Seq[V any] func(yield func(V) bool)
// yields a key-value pair per iteration
type Seq2[K, V any] func(yield func(K, V) bool)
The iterator calls yield for each value. If yield returns false, the loop has stopped (via break or return) and the iterator should exit.
A simple example
An iterator that yields integers from 0 to n-1:
func UpTo(n int) iter.Seq[int] {
return func(yield func(int) bool) {
for i := range n {
if !yield(i) {
return
}
}
}
}
// usage
for v := range UpTo(5) {
fmt.Println(v)
}
// 0 1 2 3 4
The for range loop calls your function, and your function calls yield for each value. When the loop body runs, it’s inside the yield call. When the loop breaks, yield returns false.
Iterating over a custom data structure
A linked list that’s iterable without converting to a slice first:
type Node[T any] struct {
Value T
Next *Node[T]
}
func (n *Node[T]) All() iter.Seq[T] {
return func(yield func(T) bool) {
for current := n; current != nil; current = current.Next {
if !yield(current.Value) {
return
}
}
}
}
// usage
head := &Node{Value: "a", Next: &Node{Value: "b", Next: &Node{Value: "c"}}}
for v := range head.All() {
fmt.Println(v)
}
No allocation, no intermediate slice. The loop walks the list node by node.
Key-value iterators with Seq2
Seq2 yields two values — useful for index-value or key-value pairs:
func Enumerate[T any](s []T) iter.Seq2[int, T] {
return func(yield func(int, T) bool) {
for i, v := range s {
if !yield(i, v) {
return
}
}
}
}
for i, name := range Enumerate([]string{"alice", "bob", "carol"}) {
fmt.Printf("%d: %s\n", i, name)
}
Composable iterators
Iterators compose naturally. A Filter that wraps any iterator:
func Filter[V any](seq iter.Seq[V], predicate func(V) bool) iter.Seq[V] {
return func(yield func(V) bool) {
for v := range seq {
if predicate(v) {
if !yield(v) {
return
}
}
}
}
}
// usage: even numbers from 0 to 9
for v := range Filter(UpTo(10), func(n int) bool { return n%2 == 0 }) {
fmt.Println(v)
}
// 0 2 4 6 8
A Map that transforms values:
func Map[V, R any](seq iter.Seq[V], transform func(V) R) iter.Seq[R] {
return func(yield func(R) bool) {
for v := range seq {
if !yield(transform(v)) {
return
}
}
}
}
// usage
for s := range Map(UpTo(5), strconv.Itoa) {
fmt.Println(s) // "0" "1" "2" "3" "4"
}
Chain them together:
for v := range Map(Filter(UpTo(20), isEven), double) {
fmt.Println(v)
}
Standard library support
Go 1.23 added iterator support to existing packages:
slices package:
slices.All(s) // iter.Seq2[int, E] — index and value
slices.Values(s) // iter.Seq[E] — values only
slices.Backward(s) // iter.Seq2[int, E] — reverse order
slices.Collect(seq) // []E — collect an iterator into a slice
maps package:
maps.All(m) // iter.Seq2[K, V] — all key-value pairs
maps.Keys(m) // iter.Seq[K] — keys only
maps.Values(m) // iter.Seq[V] — values only
slices.Collect is particularly useful — it materializes any iterator into a slice:
evens := slices.Collect(Filter(UpTo(20), func(n int) bool { return n%2 == 0 }))
// [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
The yield contract
The rules for writing iterators:
- Always check yield’s return value. If
yieldreturns false, stop immediately. The loop is done. - Don’t call yield after it returns false. The behavior is undefined.
- Cleanup still works.
deferstatements in the iterator run when the iterator returns, which happens when the loop ends (naturally or via break). - Iterators are single-use by convention. Calling the function returns a fresh iterator each time, but don’t reuse the same function value across multiple range loops.
When to use iterators
Lazy evaluation. When you don’t want to materialize the entire result set — reading lines from a file, paginating API results, walking a tree.
Custom data structures. Linked lists, trees, graphs — anything where the standard slice/map iteration doesn’t apply.
Composable pipelines. Filter → Map → Take → Collect chains without intermediate allocations.
When not to use them. If you already have a slice and you’re just ranging over it, for range on the slice is simpler and clearer. Don’t wrap a slice in an iterator for the sake of it.