Go cookbook · Advanced

Stream results with range-over-func iterators

Paginated APIs or database rows are loaded fully into memory before processing.

Create a free account to save this page, take notes and track your progress. Sign in

Approach

Return an iter.Seq2[T, error] that yields items page by page; callers use a plain for range loop and can stop early with break, which stops fetching further pages.

Solution

package main

import (
	"context"
	"fmt"
	"iter"
)

type Customer struct{ ID int }

// fetchPage simulates an API that returns pages of customers and a next cursor.
func fetchPage(ctx context.Context, cursor int) ([]Customer, int, error) {
	if cursor >= 9 {
		return nil, -1, nil
	}
	page := []Customer{{cursor + 1}, {cursor + 2}, {cursor + 3}}
	return page, cursor + 3, ctx.Err()
}

func AllCustomers(ctx context.Context) iter.Seq2[Customer, error] {
	return func(yield func(Customer, error) bool) {
		cursor := 0
		for cursor >= 0 {
			page, next, err := fetchPage(ctx, cursor)
			if err != nil {
				yield(Customer{}, err)
				return
			}
			for _, c := range page {
				if !yield(c, nil) {
					return // caller stopped early: don't fetch more pages
				}
			}
			cursor = next
		}
	}
}

func main() {
	for c, err := range AllCustomers(context.Background()) {
		if err != nil {
			fmt.Println("error:", err)
			break
		}
		if c.ID > 4 {
			break
		}
		fmt.Println("customer", c.ID)
	}
}

Pitfalls

  • Ignoring the result of yield; continuing after it returns false panics.
  • Hiding errors in iterators that only yield values; use iter.Seq2[T, error].
  • Holding resources (rows, connections) without releasing them when the caller breaks; use defer inside the iterator function.

Variations

  • slices.Collect(seq) materialises an iter.Seq when you do need a slice.
  • Wrap *sql.Rows in an iterator that closes the rows when iteration ends.

Further reading: iter package

Reference: https://pkg.go.dev/iter

More Go recipes

Was this page helpful?