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.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, nil) {
return
}
}
}
if err := scanner.Err(); err != nil {
yield("", err)
}
}, nil
}
This version differs from the previous in just two small ways:
- Rather than returning
iter.Seq[string]the function returnsiter.Seq2[string, error]. - We check
scanner.Err()after thescanner.Scan()loop terminates, and may yield one final value ("", err), if we encounter an error.
From the caller’s standpoint, the code changes from this:
matches, err := range grep(r, "pattern")
if err != nil {
return err
}
for match := range matches {
/* do something with the matches */
}
to:
matches, err := range grep(r, "pattern")
if err != nil {
return err
}
for match, err := range matches {
if err != nil {
return err
}
/* do something with the matches */
}
Not a big change, and generally much more natural than the alternatives, which we’ll consider in the upcoming days.
The two biggest drawbacks to this approach, in my opinion, are:
- Redundant error handling (but we can improve on that—see below)
- As discussed earlier, in the context of consuming iterators, it’s ambiguous, from the function signature, whether or not iteration errors are terminal. But if the caller assumes they are not terminal, things will still work.
To address the redundant error handling, can improve slightly on the calling semantics, by handling the possible error from regexp.Compile in the iterator. With this change, we’re left with this implementation:
func grep(r io.Reader, pattern string) iter.Seq2[string, error] {
return func(yield func(string) bool) {
re, err := regexp.Compile(pattern)
if err != nil {
yield("", err)
return
}
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := scanner.Text()
if re.MatchString(line) {
if !yield(line, nil) {
return
}
}
}
if err := scanner.Err(); err != nil {
yield("", err)
}
}, nil
}
and this caller:
for match, err := range range grep(r, "pattern") {
if err != nil {
return err
}
/* do something with the matches */
}