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.
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.