Ways to iterate in Go

Ways to iterate in Go

part of Go Cookbook
Iteration comes up everywhere: reading lines, processing SQL results, or walking a directory. Go has several ways to expose a sequence of values.
The original version of this article covered callbacks, a struct with a Next method, and channels. Since Go 1.23, functions can also be used directly in a for range loop. For a new synchronous iterator, that is a useful place to start.

Range over an iterator function

The iter package defines Seq[V] and Seq2[K, V] for sequences with one or two values per step. The iterator calls yield for each value and must return when yield returns false. That happens when the caller leaves the loop with break or return.
Here is a complete example for Go 1.23 or later. Set the module’s go directive to at least 1.23 as well.
package main

import (
	"fmt"
	"iter"
	"log"
)

func evenNumbers(max int) (iter.Seq[int], error) {
	if max < 0 {
		return nil, fmt.Errorf("max must be non-negative: %d", max)
	}
	return func(yield func(int) bool) {
		for n := 1; n <= max/2; n++ {
			if !yield(n * 2) {
				return
			}
		}
	}, nil
}

func main() {
	numbers, err := evenNumbers(10)
	if err != nil {
		log.Fatal(err)
	}
	for n := range numbers {
		fmt.Println(n)
		if n == 6 {
			break
		}
	}
}
The counter stops at max/2, avoiding an integer overflow when max is the largest int. Validation happens before iteration because this example cannot fail while producing values. For an iterator that can fail while reading, design an explicit error contract, such as a separate Err method or a sequence of (value, error) pairs.
No goroutine or channel is needed. Cleanup deferred inside the iterator runs when iteration ends, including an early exit. Do not retain yield and call it after the iterator has returned.
The standard library also provides slices.Values, maps.Keys, and slices.Collect. Map iteration order is unspecified; use slices.Sorted(maps.Keys(m)) when you need sorted keys. Go 1.24 added lazy string iterators such as strings.SplitSeq and strings.Lines.
For a simple count, Go 1.22 added integer ranges: for i := range 10 produces integers from 0 through 9. A zero or negative count produces no iterations.

Iterating via a callback

A callback is still a straightforward API when the caller needs to report an error:
func iterateEvenNumbers(max int, visit func(int) error) error {
	if max < 0 {
		return fmt.Errorf("max must be non-negative: %d", max)
	}
	for n := 1; n <= max/2; n++ {
		if err := visit(n * 2); err != nil {
			return err
		}
	}
	return nil
}
The caller supplies a function, and returning an error stops iteration. This style appears in filepath.WalkDir, which also defines special results for skipping entries. An iterator function is a similar idea with a language-supported calling convention.

Iterating with Next

An iterator object stores its position between calls. Typically, Next advances to the next value, another method retrieves that value, and Err reports a failure after the loop.
Here is the same example as a stateful iterator:
type EvenNumberIterator struct {
	max     int
	current int
	valid   bool
	done    bool
	err     error
}

func NewEvenNumberIterator(max int) *EvenNumberIterator {
	it := &EvenNumberIterator{max: max}
	if max < 0 {
		it.err = fmt.Errorf("max must be non-negative: %d", max)
	}
	return it
}

func (it *EvenNumberIterator) Next() bool {
	it.valid = false
	if it.done || it.err != nil {
		return false
	}
	if it.current/2 >= it.max/2 {
		it.done = true
		return false
	}
	it.current += 2
	it.valid = true
	return true
}

func (it *EvenNumberIterator) Value() int {
	if !it.valid {
		panic("Value requires a successful call to Next")
	}
	return it.current
}

func (it *EvenNumberIterator) Err() error { return it.err }
The caller checks the error after the loop:
it := NewEvenNumberIterator(10)
for it.Next() {
	fmt.Println(it.Value())
}
if err := it.Err(); err != nil {
	log.Fatal(err)
}
This pattern is used by sql.Rows.Next and bufio.Scanner.Scan. SQL rows also need to be closed, including when iteration stops early. The scanner for reading lines is in bufio, not go/scanner, which tokenizes Go source.

Iterating with a channel

A channel is useful when producing and consuming values should run concurrently. It adds synchronization overhead and needs a cancellation plan if the consumer stops early.
Every send, including an error result, must select on cancellation. Checking the context and then performing an unconditional send is insufficient: the consumer could stop between those operations.
type IntWithError struct {
	Int int
	Err error
}

func generateEvenNumbers(ctx context.Context, max int) <-chan IntWithError {
	ch := make(chan IntWithError)
	go func() {
		defer close(ch)
		send := func(value IntWithError) bool {
			select {
			case ch <- value:
				return true
			case <-ctx.Done():
				return false
			}
		}
		if max < 0 {
			send(IntWithError{Err: fmt.Errorf("max must be non-negative: %d", max)})
			return
		}
		for n := 1; n <= max/2; n++ {
			if !send(IntWithError{Int: n * 2}) {
				return
			}
		}
	}()
	return ch
}
Pass a non-nil context and cancel it when you stop consuming:
func printSomeEvenNumbers(max int) error {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	for value := range generateEvenNumbers(ctx, max) {
		if value.Err != nil {
			return value.Err
		}
		fmt.Println(value.Int)
		if value.Int >= 6 {
			break
		}
	}
	return nil
}
With cancellation in every send, the caller does not need to drain the channel. Cancellation is cooperative: when a send and cancellation are both ready, select may choose either. A buffered channel can also contain values sent before cancellation.

Which approach should I use?

Use a plain loop for simple local processing. Use an iterator function for a reusable synchronous sequence, a callback when its error protocol fits, or a Next API when explicit state and resource management are useful. Use channels when communication between goroutines is part of the problem.
The original examples predate range-over-function support; the examples above include the updated cancellation behavior.
#go
Sep 5 2026

Related articles

Feedback about page:

Feedback:
Optional: your email if you want me to get back to you: