Go

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. Calling yield again after it returned false makes 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 a select with ctx.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 through context.AfterFunc.
  • If you convert the iterator with iter.Pull, you must call the returned stop function (usually with defer), or the paused iterator and its resources stay alive.

More on Context

All 35 Context questions