Introduction
Go's standard iterators are push-based: the iterator drives the loop by calling yield for each value. But some algorithms need the consumer to be in control, pulling one value at a time from multiple sources. The iter.Pull function bridges this gap by converting any push iterator into a next/stop pair, giving you manual control over when and how values are consumed.
Key Concepts
- Push iterator: The standard
func(yield func(V) bool)pattern where the iterator drives the loop and pushes values to the consumer. - Pull iterator: A
next()function that the consumer calls to get values one at a time, plus astop()function for cleanup. - iter.Pull: Converts a push
iter.Seq[V]into a pull-style(next func() (V, bool), stop func())pair. - iter.Pull2: The
Seq2variant that convertsiter.Seq2[K, V]into(next func() (K, V, bool), stop func()).
Real World Context
You are building a merge-sort style algorithm that reads from two sorted streams and produces a single sorted output. With push iterators, you cannot independently advance one stream without advancing the other. With iter.Pull, you convert both streams into pull iterators, peek at the head of each, and advance only the one with the smaller value. This pattern applies to any merge, zip, or interleave operation across multiple data sources.
Deep Dive
iter.Pull takes a push iterator and returns two functions: next and stop.
gonext, stop := iter.Pull(mySeq) defer stop() // Always call stop! for { v, ok := next() if !ok { break } fmt.Println(v) }
Calling next() returns the next value and a boolean indicating whether a value was available. When ok is false, the iterator is exhausted. The defer stop() ensures cleanup runs regardless of how the loop exits.
Under the hood, iter.Pull runs the push iterator in a separate goroutine that suspends after each yield call, waiting for the consumer to call next(). This goroutine-based implementation is what makes stop() essential: without it, the goroutine hangs forever, leaking resources.
The classic use case for pull iterators is zipping two sequences together. A push iterator cannot do this because it controls the loop, and you cannot interleave two push loops. With Pull, you advance each source independently.
gofunc Zip(seq1, seq2 iter.Seq[int]) iter.Seq2[int, int] { return func(yield func(int, int) bool) { next1, stop1 := iter.Pull(seq1) defer stop1() next2, stop2 := iter.Pull(seq2) defer stop2() for { v1, ok1 := next1() v2, ok2 := next2() if !ok1 || !ok2 { return } if !yield(v1, v2) { return } } } }
This Zip function itself returns a push iterator (iter.Seq2), but internally uses Pull to advance two sources in lockstep. The deferred stop1() and stop2() calls ensure both goroutines are cleaned up.
Other use cases include interleaving values from multiple channels, implementing merge joins on sorted data, and building stateful consumers that need to pause and resume iteration at arbitrary points.
Common Pitfalls
- Forgetting to call
stop()—iter.Pullspawns a goroutine internally. If you never callstop(), that goroutine blocks forever, leaking memory and a goroutine slot. Alwaysdefer stop()immediately after callingiter.Pull. - Calling
next()afterstop()— Once you callstop(), the underlying goroutine terminates. Subsequent calls tonext()will return the zero value andfalse, but relying on this is fragile and unclear. Treatstop()as final. - Using Pull when a push iterator would suffice — Pull adds goroutine overhead. If a simple
for v := range seqworks, use it. Reserve Pull for cases where you truly need manual control over multiple iterators.
Best Practices
- Always
defer stop()on the same line asiter.Pull— This makes the cleanup obligation visible and ensures it runs even on early returns or panics. - Prefer push iterators when possible — Pull introduces goroutine synchronization overhead. Only convert to pull style when you need to interleave, zip, or manually advance multiple iterators.
Summary
- Push iterators drive the loop; pull iterators let the consumer drive.
iter.Pullconverts a pushiter.Seqinto anext/stopfunction pair by running the iterator in a goroutine.- Always call
stop()(viadefer) to avoid goroutine leaks. - Use Pull for zipping, merging, or interleaving multiple iterators where independent advancement is required.
- Prefer push iterators for simple, single-source iteration to avoid goroutine overhead.
Code Examples
func Zip(seq1, seq2 iter.Seq[int]) iter.Seq2[int, int] {
return func(yield func(int, int) bool) {
next1, stop1 := iter.Pull(seq1)
defer stop1()
next2, stop2 := iter.Pull(seq2)
defer stop2()
for {
v1, ok1 := next1()
v2, ok2 := next2()
if !ok1 || !ok2 {
return
}
if !yield(v1, v2) {
return
}
}
}
}