How do you make a range-over-func iterator (iter.Seq) respect context cancellation, and why is it better than a channel-based generator?
Question 292HardGo 1.22 to 1.25
Since Go 1.23, iterators are plain functions that call yield. The loop body runs synchronously on the caller's goroutine, so there is no producer goroutine to leak. Take the context as a parameter of the constructor, check it between elements, and report the cause through a Seq2[T, error].
func Lines(ctx context.Context, r io.Reader) iter.Seq2[string, error] {
return func(yield func(string, error) bool) {
sc := bufio.NewScanner(r)
for sc.Scan() {
if ctx.Err() != nil {
yield("", context.Cause(ctx))
return
}
if !yield(sc.Text(), nil) {
return // consumer did break/return: stop immediately
}
}
if err := sc.Err(); err != nil {
yield("", err)
}
}
}
func count(ctx context.Context, f *os.File) (int, error) {
n := 0
for _, err := range Lines(ctx, f) {
if err != nil {
return n, err
}
n++
}
return n, nil
}
Points interviewers probe:
- Honor
yield's return value. Callingyieldagain after it returnedfalsemakes the runtime panic. - Channel generators need context to avoid leaks. A
go func() { for ... { ch <- v } }()producer blocks forever when the consumer stops reading, unless every send is in aselectwithctx.Done(). Push iterators avoid that goroutine completely. - A cancellation check only runs between elements. If one step blocks (here,
sc.Scan()on a network reader), you still need a context-aware reader, or a deadline set throughcontext.AfterFunc. - If you convert the iterator with
iter.Pull, you must call the returnedstopfunction (usually withdefer), or the paused iterator and its resources stay alive.
More on Context
- Q290What does this print? (AfterFunc on already-canceled and live contexts)
- Q291What does this print? (Cause across WithoutCancel)
- Q293How do semaphore.Weighted and rate.Limiter use context, and what subtle bugs appear with them?