Building a custom iterator

October 31, 2025

We’ve looked at using channels as iterators, and found they’re hardly ideal. Let’s look at the next obvious answer: custom iterators.

Before range-over-func, which we’ll get to next, custom iterators were really the only meaningful solution. And they still remain a very viable one, because of their great flexibility.

Let’s start with a simple implementation of a custom iterator version of our grep function (see it in the playground):

type Result struct {
	re      *regexp.Regexp
	scanner *bufio.Scanner
}

func (r *Result) Next() (string, bool) {
	for r.scanner.Scan() {
		line := r.scanner.Text()
		if r.re.MatchString(line) {
			return line, true
		}
	}
	return "", false
}

func (r *Result) Err() error {
	return r.scanner.Err()
}

func grep(r io.Reader, pattern string) (*Result, error) {
	re, err := regexp.Compile(pattern)
	if err != nil {
		return nil, err
	}
	return &Result{
		re:      re,
		scanner: bufio.NewScanner(r),
	}, nil
}

This is quite similar to our original, non-iterating implementation. The main difference is that it’s broken into three parts:

  1. Setup (compile regular expression, create scanner)
  2. Iteration (read each result, one at a time)
  3. Cleanup (error checking)

The three parts are tied together with the new Result type, which is instantiated during the setup stage. Let’s look at the iteration part in detail, which happens inside the Next() method, as that’s where the magic happens.

func (r *Result) Next() (string, bool) {
	for r.scanner.Scan() {
		line := r.scanner.Text()
		if r.re.MatchString(line) {
			return line, true
		}
	}
	return "", false
}

There are three key things to notice that this function does:

  1. It advances the underlying state to the next value.
  2. It returns that value.
  3. It indicates when there are no more values.

The first of these three is a bit subtle, as we aren’t doing any explicit state tracking, as is sometimes necessary. Here that state is managed by the scanner.Scan() method, which we’re just calling. Here we call that method in a loop. This means each call to Result.Next() may result in many calls to r.scanner.Scan(), since we keep calling that method repeatedly until we find a line that matches the expected regular expression, then we return the matching line, along with a bool value of true to indicate a successful match.

Once we find a match, we return immediately (breaking out of the loop). At that point, the state of the scanner is preserved, and ready for the next call to Next().

Once the scanner reads all lines of input, the for loop ends, and Next() returns an empty string and bool value of false to indicate no match.

Some readers may be wondering if the inclusion of the bool value is necessary. In this case it is, because a blank line could potentially match a regular expression. But in some cases, it would not be necessary–particularly when returing a pointer value, it may be sufficient to use a nil value to indicate “no more results”.


Share this

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

Unsure? Browse the archive .

Related Content


Separating iteration from advancement

Last week we looked at a simple custom iterator pattern: type Result struct {/* ... */} func (*Result) Next() (string, bool) func (*Result) Err() error But let’s talk about a few variations, and when they might make sense. First, I already mentioned last week that in some cases we could simply eliminate the bool value if the zero value of the iterated value can serve as an indication that iteration has completed.


Custom iterators

Until recently, custom iterators were probably the most common way to iterate over a list of elements that might trigger an error. Several examples exist in the standard library. Perhaps the most well known would be the sql.Rows type, which provides (among others), the following methods: Next() bool — Advances to the next item Scan(...any) error — Processes the current item Err() error — Reports an iteration error Close() error — Closes iterator, possibly before the last item has been processed These four methods are pretty standard, in any custom iterator implementation, though Scan() will typically be replaced with an implementation-specific method to process the current result, and in some cases Err() and Close() may be combined.


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