Implementing a range-over-func iterator

November 12, 2025

It’s finally time to look at how we implement a range-over-func iterator. I’d venture a guess this should be the go-to pattern for most iterators, unless or until you have special needs that it won’t address. And, of course, we’ll discuss some of those in the near future, as well.

First, here’s how our grep implementation looks using range-over-func:

func grep(r io.Reader, pattern string) (iter.Seq[string], error) {
  re, err := regexp.Compile(pattern)
  if err != nil {
    return nil, err
  }
  scanner := bufio.NewScanner(r)
  return func(yield func(string) bool) {
    for scanner.Scan() {
      line := scanner.Text()
      if re.MatchString(line) {
        if !yield(line) {
          return
        }
      }
    }
  }, nil
}

Actually, this implementation is incomplete. It ignores any error from the scanner—note the absence of if err := scanner.Err(); ..., we’ll discuss that soon, but first let’s make sure we understand the code that is there.

Here’s what the above code does:

  • As before, this implementation of grep compiles the pattern, and if that fails it returns an error immediately, without iterating.
  • Also, as before, it creates a scanner object, then begins looping over its results.
  • Each time a matching line is found, the yield callback is called, with the matching line as an argument: yield(line). That yield callback is handled specially by the Go runtime, and converted into the next value in the caller’s range
  • If the yield function returns false, that means the caller exited their for ... range loop early, and the iterator can do any necessary cleanup (and should not call yield again)

It’s pretty straight-forward, in my opinion, despite the rather opaque appearance of the iter.Seq type at first.

But as mentioned, there’s one big missing piece here: What if the scanner encounters an error? Our current implementation ignores that. We’ll look at two possible solutions in the next installment…


Share this

Direct to your inbox, daily. I respect your privacy .

Unsure? Browse the archive .

Related Content


Iterator callbacks

Today I want to expand with a thought I touched on yesterday: Callbacks with iterators. Yesterday’s context was error handling. But I’ve found callbacks with iterators to be very valuable in a slightly different case. To understand the problem callbacks have helped me solve, we need to move away from our grep example to something a bit more involved. Let’s imagine we’re querying a REST API, which returns paginated results. A typical response body might look something like this, in JSON:


Alternatives to iter.Seq3

Last time we modified our range-over-func iterator to return a iter.Seq2, so that it could include a possible error value for each iteration. But this isn’t the only way to handle errors with range-over-func. And in fact, in some cases, it may not even be possible! Suppose you’re ranging over a key/value pair, for example. There is no iter.Seq3 option to return three values per iteration. To illustrate, let’s update our grep to return the line number, and the matching line:


Handling errors during iteration with range-over-func

Yesterday we looked at a range-over-func iterator that was missing a vital piece: Error handling during iteration. I know of three possible solutions to this problem, and today we’ll look at the simplest of them, which I typically recommend: Using iter.Seq2. We’ve already looked at this pattern from the consumer’s perspective. Today we’ll see how the implementation works. func grep(r io.Reader, pattern string) (iter.Seq2[string, error], error) { re, err := regexp.

Get daily content like this in your inbox!

Subscribe