Skip to main content
Starting with version 1.23, Go added support for iterators, which lets you range over pretty much anything using the range keyword.

From generics to iterators

In the generics example, we had an AllElements method that returned a slice. With iterators, we can do better:
package main

import (
    "fmt"
    "iter"
    "slices"
    "strings"
)

type List[T any] struct {
    head, tail *element[T]
}

type element[T any] struct {
    next *element[T]
    val  T
}

func (lst *List[T]) Push(v T) {
    if lst.tail == nil {
        lst.head = &element[T]{val: v}
        lst.tail = lst.head
    } else {
        lst.tail.next = &element[T]{val: v}
        lst.tail = lst.tail.next
    }
}

Creating an iterator

// All returns an iterator, which in Go is a function with a special signature.
func (lst *List[T]) All() iter.Seq[T] {
    return func(yield func(T) bool) {
        // The iterator function takes another function called yield.
        // It will call yield for every element we want to iterate over,
        // and note yield's return value for potential early termination.
        for e := lst.head; e != nil; e = e.next {
            if !yield(e.val) {
                return
            }
        }
    }
}
An iterator in Go is a function with the signature func(yield func(T) bool). The iter.Seq[T] type from the standard library defines this signature.

Using iterators

func main() {
    lst := List[int]{}
    lst.Push(10)
    lst.Push(13)
    lst.Push(23)

    // Since List.All returns an iterator, we can use it in a regular range loop.
    for e := range lst.All() {
        fmt.Println(e)
    }

    // Packages like slices have utilities for working with iterators.
    // Collect takes any iterator and collects all its values into a slice.
    all := slices.Collect(lst.All())
    fmt.Println("all:", all)
}
Output:
10
13
23
all: [10 13 23]

Infinite iterators

Iteration doesn’t require an underlying data structure and doesn’t even have to be finite:
// genFib returns an iterator over Fibonacci numbers.
// It keeps running as long as yield keeps returning true.
func genFib() iter.Seq[int] {
    return func(yield func(int) bool) {
        a, b := 0, 1

        for {
            if !yield(a) {
                return
            }
            a, b = b, a+b
        }
    }
}

func main() {
    for n := range genFib() {
        // Once the loop hits break or an early return,
        // the yield function will return false.
        if n >= 10 {
            break
        }
        fmt.Println(n)
    }
}
Output:
0
1
1
2
3
5
8

Standard library iterators

// Standard library packages expose iterator helpers.
// For example, strings.SplitSeq iterates over parts without
// first building a result slice.
for part := range strings.SplitSeq("go-by-example", "-") {
    fmt.Printf("part: %s\n", part)
}
Output:
part: go
part: by
part: example

How iterators work

1

Define the iterator function

Return a function with signature func(yield func(T) bool):
func (lst *List[T]) All() iter.Seq[T] {
    return func(yield func(T) bool) {
        // ...
    }
}
2

Call yield for each element

Inside the iterator, call yield for every value you want to produce:
for e := lst.head; e != nil; e = e.next {
    if !yield(e.val) {
        return  // Stop if yield returns false
    }
}
3

Handle early termination

Check yield’s return value. false means the caller used break or return:
if !yield(e.val) {
    return  // Clean up and exit
}

Iterator types

The iter package defines two main iterator types:
type Seq[V any] func(yield func(V) bool)
Iterates over single values. Used with single-variable range loops:
for v := range mySeq {
    fmt.Println(v)
}

Benefits of iterators

Memory efficient

No need to allocate intermediate slices. Values are produced on demand.

Lazy evaluation

Computation happens only as values are consumed.

Infinite sequences

Can represent unbounded sequences that are consumed incrementally.

Composable

Can be chained and transformed using helper functions.

Working with iterators

import "slices"

items := slices.Collect(myIterator)
func Filter[T any](seq iter.Seq[T], predicate func(T) bool) iter.Seq[T] {
    return func(yield func(T) bool) {
        for v := range seq {
            if predicate(v) {
                if !yield(v) {
                    return
                }
            }
        }
    }
}
func Map[T, U any](seq iter.Seq[T], f func(T) U) iter.Seq[U] {
    return func(yield func(U) bool) {
        for v := range seq {
            if !yield(f(v)) {
                return
            }
        }
    }
}

Best practices

  • Return iterators for data structures instead of materializing slices
  • Check the yield return value and stop early if it returns false
  • Use iterators for large or infinite sequences
  • Leverage standard library functions like slices.Collect and strings.SplitSeq
Iterators are particularly useful for large datasets where you don’t want to allocate the entire result in memory.

Generics

Type parameters for custom types

Range

Range over built-in types

iter package

Standard library documentation

Build docs developers (and LLMs) love