Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Go  Module 12 – Iterators and range-over-func

Pull iterators and lazy pipelines with generics

Pull values on demand with iter.Pull and always call stop, write generic Filter, Map and Take adapters, and choose iterators, channels or slices for a pipeline.

  • Advanced
  • 20 minutes
  • Examples run with Go 1.27.1 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 8 of 20 minutes free
Free: iter.Pull turns a push iterator into next and stop · Adapters wrap one iterator in another, and do only the work asked of them. With a pass: 2 in-depth parts and 3 interview answers, about 12 minutes.

What you will learn

  • Convert a push iterator to pull form with iter.Pull, and call stop on every path
  • Write generic Filter, Map and Take adapters that stop their source as soon as the loop stops
  • Walk two sequences in step with iter.Pull, to zip them or to merge two sorted ones
  • Define a Stream type with methods, including a generic Map method of Go 1.27
  • Choose between iterators, pulls, channels and slices for a pipeline, and avoid the leaks of channel generators

Before you start

What you’ll learn · Before you start

What you will learn

  • Convert a push iterator to pull form with iter.Pull, and call stop on every path
  • Write generic Filter, Map and Take adapters that stop their source as soon as the loop stops
  • Walk two sequences in step with iter.Pull, to zip them or to merge two sorted ones
  • Define a Stream type with methods, including a generic Map method of Go 1.27
  • Choose between iterators, pulls, channels and slices for a pipeline, and avoid the leaks of channel generators

Before you start

On this page

Finding the PNRs that appear in two sorted lists of 10,000 needs both lists to move in step, but a range loop drives one iterator at a time, and the iterator decides when its next value comes. Pull iterators turn that around: the consumer asks for each value when it wants one.

You will pull values with iter.Pull and release them with stop, then build lazy pipelines with generics from small adapters such as Filter, Map and Take, and see how much work they skip. Each example was run on Go 1.27.1, and its printout is shown as recorded.

iter.Pull turns a push iterator into next and stop

iter.Pull(seq) returns two functions. Each call of next returns the sequence’s next value and true, or the zero value and false once the sequence is over. stop ends the iteration early and releases what it holds. A pull iterator: An iterator the consumer calls for each value: next returns the value and whether there was one. iter.Pull makes one from a push iterator, with a stop function that must run if the sequence is not read to its end. like this suits a consumer that is not a single loop: one that reads two sequences in step, or takes one value at a time from code that is driven by something else. Pull iterators come in two shapes, iter.Pull for an iter.Seq and iter.Pull2 for an iter.Seq2. This program pulls from Coaches twice, once to the end and once with an early stop:

pull.go: next until the end, and stop after one value Go · pull.go
// pull.go: iter.Pull turns a push iterator into a next function and a stop function.
package main

import (
	"fmt"
	"iter"
)

// Coaches yields two coach codes and says when it returns.
func Coaches() iter.Seq[string] {
	return func(yield func(string) bool) {
		defer fmt.Println("  (Coaches returned)")
		for _, c := range []string{"S4", "B1"} {
			if !yield(c) {
				return
			}
		}
	}
}

func main() {
	next, stop := iter.Pull(Coaches())
	defer stop()
	for range 3 {
		c, ok := next()
		fmt.Printf("next() = %q, %v\n", c, ok)
	}

	fmt.Println("a second pull, stopped after one value:")
	next2, stop2 := iter.Pull(Coaches())
	c, ok := next2()
	fmt.Printf("next() = %q, %v\n", c, ok)
	stop2()
	c, ok = next2()
	fmt.Printf("next() after stop = %q, %v\n", c, ok)
}

Output

next() = "S4", true
next() = "B1", true
  (Coaches returned)
next() = "", false
a second pull, stopped after one value:
next() = "S4", true
  (Coaches returned)
next() after stop = "", false

Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run pull.go

The first pull gave S4 and B1, then Coaches returned, and the third next gave “” and false. The second pull read S4 and called stop: Coaches returned at once, during stop, and next then gave “” and false. Step through the first pull:

Interactive · message by message Three calls of next on a pulled Coaches Step through the first pull of pull.go and watch who runs when.
Three calls of next on a pulled Coaches, step 7 of 7. Messages so far: main to next and stop: next(); Coaches() to next and stop: yield("S4"); next and stop to main: "S4", true; next and stop to main: "B1", true; next and stop to main: "", false.maincalls next and stopnext and stopready, nothing runCoaches()returnednext()yield("S4")"S4", true"B1", true"", false

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. iter.Pull returns two functions. iter.Pull(Coaches()) returns next and stop and runs nothing yet. main defers stop, so the iterator is released however main ends.
  2. The first next. main calls next. The iterator starts and runs until its first yield.
  3. The iterator yields S4. Coaches calls yield("S4"). Instead of running a loop body, yield hands S4 to next and the iterator waits inside yield.
  4. next returns S4, true. next returns "S4" and true to main, which prints it. The iterator stays paused until main asks again.
  5. The second next. The second call resumes Coaches where it waited. It yields B1, and next returns "B1" and true.
  6. The sequence ends. On the third call Coaches has no value left, so it returns and prints (Coaches returned). next returns the zero value "" and false.
  7. stop is still called. main's deferred stop runs when main returns. The sequence had ended, so there is nothing left to release, and calling stop is harmless.

The package states the rule: a consumer that does not read the sequence to its end must call stop. Writing defer stop() on the line after iter.Pull covers every path, and calling stop after the end is harmless.

Checkpoint

Check: next and stop from iter.Pull

  1. Question 1 of 2 Coaches yields "S4" and "B1". What does the third call of next return after next, stop := iter.Pull(Coaches())?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: "", false

    next returns each value with true, then the zero value with false. pull.go printed next() = "", false on the third call.

  2. Question 2 of 2 When must a consumer of iter.Pull call stop?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Whenever it does not read the sequence to the end; defer stop() covers every case

    Read to the end, or call stop. Writing defer stop() on the line after iter.Pull does both safely.

Adapters wrap one iterator in another, and do only the work asked of them

Filter, Map and Take each take a sequence and return a new one that ranges over it. With type parameters, one Filter serves every element type and Map can change the type, from seat numbers to text. adapters.go chains the three over a million seats and counts what the source had to make:

adapters.go: Take(Map(Filter(Seats(1,000,000)))) and what Seats made Go · adapters.go
// adapters.go: generic Filter, Map and Take wrap one iterator in another, and do only the work the loop asks for.
package main

import (
	"fmt"
	"iter"
)

// Filter yields the values of seq for which keep returns true.
func Filter[V any](seq iter.Seq[V], keep func(V) bool) iter.Seq[V] {
	return func(yield func(V) bool) {
		for v := range seq {
			if keep(v) && !yield(v) {
				return
			}
		}
	}
}

// Map yields f(v) for each value v of seq.
func Map[V, W any](seq iter.Seq[V], f func(V) W) iter.Seq[W] {
	return func(yield func(W) bool) {
		for v := range seq {
			if !yield(f(v)) {
				return
			}
		}
	}
}

// Take yields the first n values of seq, then returns, which stops seq too. It counts with a variable of each walk's
// own, left, so the iterator it returns can be walked more than once.
func Take[V any](seq iter.Seq[V], n int) iter.Seq[V] {
	return func(yield func(V) bool) {
		left := n
		if left <= 0 {
			return
		}
		for v := range seq {
			if !yield(v) {
				return
			}
			if left--; left == 0 {
				return
			}
		}
	}
}

// Seats yields seat numbers 1 to n and counts how many it produced.
func Seats(n int, produced *int) iter.Seq[int] {
	return func(yield func(int) bool) {
		for seat := 1; seat <= n; seat++ {
			*produced++
			if !yield(seat) {
				return
			}
		}
	}
}

func main() {
	produced := 0
	window := func(seat int) bool { return seat%2 == 1 } // odd seats are by the window in this coach
	label := func(seat int) string { return fmt.Sprintf("window seat %d", seat) }

	for s := range Take(Map(Filter(Seats(1_000_000, &produced), window), label), 3) {
		fmt.Println(s)
	}
	fmt.Println("Seats produced", produced, "of 1,000,000")
}

Output

window seat 1
window seat 3
window seat 5
Seats produced 5 of 1,000,000

Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run adapters.go

The loop printed three window seats, and Seats produced 5 of its 1,000,000. Lazy pipelines with generics like this one do only what the loop at the end asks for. Each adapter runs its source only while its own yield keeps returning true. Follow the five seats through the stages:

Interactive · stage by stage Five seats through Filter, Map and Take Step through adapters.go's pipeline and count what Seats has to produce.
Five seats through Filter, Map and Take, step 6 of 6. Showing Seats(1,000,000) (produced 5), Filter(window), Map(label), Take(3) (returned after 3), The for loop. In focus: Seats(1,000,000).Seats(1,000,000)produced 5Filter(window)keeps the odd seatsMap(label)turns a seat into textTake(3)returned after 3The for loopprints each one

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The loop asks Take. The loop ranges over Take, which ranges over Map, which ranges over Filter, which ranges over Seats. Nothing is produced until Seats is asked for its first seat.
  2. Seat 1 goes all the way. Seats produces 1. Filter keeps it, Map turns it into "window seat 1", Take passes it on, and the loop prints it.
  3. Seat 2 stops at Filter. Seats produces 2. Filter drops it and asks Seats for the next seat; Map, Take and the loop never see it.
  4. Seats 3 and 4. Seat 3 reaches the loop as "window seat 3"; seat 4 is dropped by Filter, as seat 2 was.
  5. Seat 5 fills Take. Seat 5 reaches the loop as "window seat 5". That was Take's third value, so Take returns instead of asking for more.
  6. False runs back up. Take's return makes the yield that Map called return false, so Map returns; the same reaches Filter and then Seats. Seats stopped at 5 of 1,000,000 seats.

Counting down a captured parameter

In Take, writing if n--; n == 0 changes the parameter n, which every walk of the returned iterator shares, so the second walk starts at 0 and yields nothing. Copy it inside the function, left := n, as adapters.go does.

The rest of this lesson builds on these two ideas. Zip pairs two sequences and Merge interleaves two sorted ones, both by pulling. A Stream type gives a pipeline methods, and Go 1.27 lets its Map method change the element type.

Iterators are one of three ways to pass values from one stage of a program to the next. A channel carries values from a goroutine that produces them to another that consumes them, so the two can run at the same time. The producing goroutine leaks when its consumer stops early and nothing tells it to stop. A slice holds every value at once, so it costs memory for all of them before the first is read. In exchange, a slice can be read as many times as the program needs.

Checkpoint

Check: Lazy generic adapters

  1. Question 1 of 2 adapters.go's pipeline takes 3 odd seats and Seats produced 5. How many seats does Seats produce for Take(Filter(Seats(1_000_000), odd), 10)?

    Type a number.

    A hint

    The tenth odd seat number is the last one Seats has to make.

    Show the answer: Question 1 of 2

    Answer: 19 seats

    The odd seats are 1, 3, 5 and so on; the tenth is 19. Take returns after it, each stage it reads from returns in turn, and Seats has produced 19 seats, not 1,000,000.

  2. Question 2 of 2 Which pipeline makes Seats produce fewer values, when both print the first 3 window seats as text?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Neither; both stop Seats at 5

    Every adapter stops when its consumer stops. What Seats makes depends on how many values reach Take, which only Filter changes.

Two sequences in step: Zip and Merge, and what a forgotten stop costs Pro: opens with a Pro, Premium or Ultimate pass

Walking two sequences in step takes at least one pull. This part writes Zip and Merge with pulls, shows both stops running when a loop breaks, and counts the goroutines that 1,000 pulls hold when nobody calls stop.

In this part:

  • Zip and Merge, built on one pull or two
  • What a pull left without stop costs, measured

2 runnable examples (Go) · about 5 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

A Stream type with Go 1.27 generic methods, and iterators against channels Pro: opens with a Pro, Premium or Ultimate pass

Pipelines read better from left to right, and channels look like the obvious way to stream values. This part gives a sequence type methods, including the generic Map that Go 1.27 allows, then measures what a channel generator costs and leaks against an iterator and a pull.

In this part:

  • A Stream type with Filter, Take and a generic Map method
  • A channel generator leaks when its consumer stops early, measured
  • Choosing iterators, pulls, channels or slices for a pipeline

3 runnable examples (Go) · about 9 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Practise: three adapters

practice/adapters.go has three adapters to write, and practice/adapters_test.go tests them, including a second walk of one Skip and a source that counts what TakeWhile asked for:

practice/adapters.go and practice/adapters_test.go

Go (adapters.go) · practice/adapters.go

// Package practice holds three adapters for you to write. Each must stop as soon as its yield returns false.
package practice

import "iter"

// Skip yields the values of seq after the first n.
func Skip[V any](seq iter.Seq[V], n int) iter.Seq[V] {
	return func(yield func(V) bool) {
		// write it
	}
}

// TakeWhile yields the values of seq while ok returns true, and stops at the first value for which it is false,
// without asking seq for any more.
func TakeWhile[V any](seq iter.Seq[V], ok func(V) bool) iter.Seq[V] {
	return func(yield func(V) bool) {
		// write it
	}
}

// Enumerate yields each value of seq with its position, counting from 0.
func Enumerate[V any](seq iter.Seq[V]) iter.Seq2[int, V] {
	return func(yield func(int, V) bool) {
		// write it
	}
}

Go (adapters_test.go) · practice/adapters_test.go

package practice

import (
	"fmt"
	"iter"
	"slices"
	"testing"
)

// counted yields 1 to n and counts how many values it produced.
func counted(n int, produced *int) iter.Seq[int] {
	return func(yield func(int) bool) {
		for i := 1; i <= n; i++ {
			*produced++
			if !yield(i) {
				return
			}
		}
	}
}

func TestSkip(t *testing.T) {
	got := slices.Collect(Skip(slices.Values([]int{545, 1450, 2050, 3420}), 2))
	if fmt.Sprint(got) != "[2050 3420]" {
		t.Errorf("Skip(…, 2) = %v, want [2050 3420]", got)
	}
}

func TestSkipTwice(t *testing.T) {
	later := Skip(slices.Values([]int{545, 1450, 2050, 3420}), 2)
	first, second := slices.Collect(later), slices.Collect(later)
	if fmt.Sprint(first, second) != "[2050 3420] [2050 3420]" {
		t.Errorf("two walks of one Skip gave %v and %v, want [2050 3420] both times", first, second)
	}
}

func TestTakeWhileStopsAsking(t *testing.T) {
	produced := 0
	got := slices.Collect(TakeWhile(counted(1_000, &produced), func(n int) bool { return n < 4 }))
	if fmt.Sprint(got) != "[1 2 3]" || produced != 4 {
		t.Errorf("TakeWhile(n < 4) = %v after %d values, want [1 2 3] after 4", got, produced)
	}
}

func TestEnumerate(t *testing.T) {
	var got []string
	for i, name := range Enumerate(slices.Values([]string{"Asha", "Ravi", "Meena"})) {
		got = append(got, fmt.Sprint(i, ":", name))
		if i == 1 {
			break // a correct Enumerate returns now; one that ignores false makes the loop panic
		}
	}
	if fmt.Sprint(got) != "[0:Asha 1:Ravi]" {
		t.Errorf("Enumerate with a break at 1 gave %v, want [0:Asha 1:Ravi]", got)
	}
}

The starter code compiles, and every test fails:

The starter adapters against their tests Python · practice_session.py
from session import run

# The starter code of practice/ compiles, and every test fails until the three adapters are written.
run(
    """
cd practice && go test .
""",
    expect_failure=["cd practice && go test ."],
)

Output

$ cd practice && go test .
--- FAIL: TestSkip (0.00s)
    adapters_test.go:25: Skip(…, 2) = [], want [2050 3420]
--- FAIL: TestSkipTwice (0.00s)
    adapters_test.go:33: two walks of one Skip gave [] and [], want [2050 3420] both times
--- FAIL: TestTakeWhileStopsAsking (0.00s)
    adapters_test.go:41: TakeWhile(n < 4) = [] after 0 values, want [1 2 3] after 4
--- FAIL: TestEnumerate (0.00s)
    adapters_test.go:54: Enumerate with a break at 1 gave [], want [0:Asha 1:Ravi]
FAIL
FAIL	example.com/practice	2.982s
FAIL
[exit status 1]

This output changes from run to run: go test prints how long each test and the whole run took, which differs from one run to the next.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 practice_session.py

Make go test . pass in the practice folder. A first hint: Skip needs a counter of each walk’s own, skip := n inside the function it returns, or TestSkipTwice catches the shared parameter.

The questions below cover pulls and adapters, and each one is explained as soon as you answer it.

Practice that adapts to you

Practise this lesson

8 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 8 What does next return when an iter.Seq[int] is over?

    Choose one answer.

    Show the answer: Question 1 of 8

    Answer: 0, false

    The pair (zero value, false) means the sequence is over; every later call returns the same.

  2. Question 2 of 8 What does pull.go print?

    What does this program print? Choose one answer.

    // pull.go: iter.Pull turns a push iterator into a next function and a stop function.
    package main
    
    import (
    	"fmt"
    	"iter"
    )
    
    // Coaches yields two coach codes and says when it returns.
    func Coaches() iter.Seq[string] {
    	return func(yield func(string) bool) {
    		defer fmt.Println("  (Coaches returned)")
    		for _, c := range []string{"S4", "B1"} {
    			if !yield(c) {
    				return
    			}
    		}
    	}
    }
    
    func main() {
    	next, stop := iter.Pull(Coaches())
    	defer stop()
    	for range 3 {
    		c, ok := next()
    		fmt.Printf("next() = %q, %v\n", c, ok)
    	}
    
    	fmt.Println("a second pull, stopped after one value:")
    	next2, stop2 := iter.Pull(Coaches())
    	c, ok := next2()
    	fmt.Printf("next() = %q, %v\n", c, ok)
    	stop2()
    	c, ok = next2()
    	fmt.Printf("next() after stop = %q, %v\n", c, ok)
    }
    Show the answer: Question 2 of 8

    Answer: it prints

    next() = "S4", true
    next() = "B1", true
      (Coaches returned)
    next() = "", false
    a second pull, stopped after one value:
    next() = "S4", true
      (Coaches returned)
    next() after stop = "", false

    A pull runs the iterator a little at a time; the iterator returns at the end of its values or when stop is called, and its deferred calls run then.

  3. Question 3 of 8 This function returns as soon as it finds an error. Which line belongs where the comment is?

    Read the code, then choose one answer.

    func firstBad(results iter.Seq[Result]) error {
    	next, stop := iter.Pull(results)
    	// ?
    	for r, ok := next(); ok; r, ok = next() {
    		if r.Err != nil {
    			return r.Err
    		}
    	}
    	return nil
    }
    Show the answer: Question 3 of 8

    Answer: defer stop()

    Any early return leaves a pull holding its iterator. defer stop() on the line after iter.Pull releases it on every path, and costs nothing when the loop reaches the end.

  4. Question 4 of 8 Which task needs iter.Pull rather than a range loop?

    Choose one answer.

    Show the answer: Question 4 of 8

    Answer: Pairing the values of two sequences, one from each, in order

    Pull iterators are for consumers that are not one loop over one sequence, above all for two sequences in step.

  5. Question 5 of 8 What does Filter(seq, keep) return?

    Choose one answer.

    Show the answer: Question 5 of 8

    Answer: A new iter.Seq that yields the values of seq for which keep returns true

    Adapters take a sequence and return a sequence, so they can be chained without building anything in between.

  6. Question 6 of 8 For Take(Filter(Seats(1_000_000), func(s int) bool { return s%3 == 0 }), 4), how many seats does Seats produce?

    Type a number.

    A hint

    Find the fourth multiple of 3.

    Show the answer: Question 6 of 8

    Answer: 12 seats

    The seats that pass are 3, 6, 9 and 12. Take returns after the fourth, each stage it reads from returns in turn, and Seats has produced 12 seats.

  7. Question 7 of 8 This Take works the first time it is walked. What does a second walk of the same returned iterator yield?

    Read the code, then choose one answer.

    func Take[V any](seq iter.Seq[V], n int) iter.Seq[V] {
    	return func(yield func(V) bool) {
    		for v := range seq {
    			if n <= 0 || !yield(v) {
    				return
    			}
    			n--
    		}
    	}
    }
    Show the answer: Question 7 of 8

    Answer: Nothing, because the first walk counted the shared n down to 0

    A closure that changes a captured parameter changes it for every later walk. Count with a variable declared inside the function the adapter returns.

  8. Question 8 of 8 Why can Map(seq, label) turn an iter.Seq[int] into an iter.Seq[string]?

    Choose one answer.

    Show the answer: Question 8 of 8

    Answer: Map has two type parameters, V for what it reads and W for what it yields

    Generic adapters keep pipelines type-safe: each stage states the type it reads and the type it yields, and the compiler checks the chain.

Interview questions

Warm-up (2 to 5 years): What do the two functions that iter.Pull returns do?

next returns the sequence’s next value and true, or the zero value and false when the sequence is over. stop ends the iteration early: it makes the iterator return, running its deferred calls, and releases what the pull holds. A consumer that does not read to the end must call stop, so write defer stop() right after iter.Pull.

Three longer questions of the module bank build on this lesson:

  1. Mid to senior

    A service compares two result sets per request by zipping them with iter.Pull. After a release, its goroutine count climbs all day and memory follows, though traffic is flat. What is the likely cause, how do you prove it, and how do you fix and guard it?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to staff

    An older package exposes func Bookings(day Date) <-chan Booking, backed by a goroutine that reads a database, and callers often stop after the first match. Explain what goes wrong, redesign the API with iterators, and say when you would keep a channel.

    Model answer, Pro. First step and follow-ups

  3. Mid to senior

    Write Merge(a, b iter.Seq[int]) iter.Seq[int] that merges two sorted sequences lazily into one sorted sequence. Walk through its edge cases, what it costs, and how you would test that it stops both inputs when the caller breaks.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • iter.Pull turns a push iterator into next and stop; a consumer that stops early must call stop, and defer stop() right after the call covers every path.
  • Generic adapters such as Filter, Map and Take wrap one sequence in another, and a pipeline of them does only the work its last loop asks for: Seats made 5 of 1,000,000 values for Take(3).
  • An adapter that counts down a captured parameter works once; a counter of each walk’s own makes the iterator reusable.

Terms in this lesson

pull iterator
An iterator the consumer calls for each value: next returns the value and whether there was one. iter.Pull makes one from a push iterator, with a stop function that must run if the sequence is not read to its end.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.