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
grepcompiles 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
yieldcallback is called, with the matching line as an argument:yield(line). Thatyieldcallback is handled specially by the Go runtime, and converted into the next value in the caller’srange - If the
yieldfunction returns false, that means the caller exited theirfor ... rangeloop early, and the iterator can do any necessary cleanup (and should not callyieldagain)
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…