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:
func grep(r io.Reader, pattern string) (iter.Seq2[int64, string], error) {
re, err := regexp.Compile(pattern)
if err != nil {
return nil, err
}
var lineNumber int64
scanner := bufio.NewScanner(r)
return func(yield func(string) bool) {
lineNumber++
for scanner.Scan() {
line := scanner.Text()
if re.MatchString(line) {
if !yield(lineNumber, line) {
return
}
}
}
}, nil
}
Notice we once again have omitted the check for scanner.Err()—we’ll get back to that next. But first, just to illustrate how our caller now works:
result, err := grep(r, pattern)
if err != nil {
return err
}
for lineNo, line := range result {
fmt.Printf("Line %d matches: %s\n", lineNo, line)
}
So… how do we get our error handling back?
I know of three options:
-
Combine our results into a single struct, so that we can use the second value of
iter.Seq2for errors again. e.g.:type Match struct { LineNumber int64 Line string } func grep(r io.Reader, pattern string) iter.Seq2[Result, error] { -
Arrange for an
Err()method to be called after the iterator has exited. This can be clumsy, but in some cases—usually complicated ones, where you have special requirements for extra methods—it’s the best option. but one approach would be to makegrepreturn aResultobject with anIter()method and aErr()method.type Matches struct { /* private fields */ } // Iter returns an iterator. func (m *Matches) Iter() iter.Seq2[int64, string] // Err returns a error, if one occurred, during iteration. Should be called // after Iter() is consumed. func (m *Matches) Err() error(I’m omitting the implementation of this one, because it should probably very rarely be used. But if you’d like to see how to implement it, hit reply and let me know. I can dedicate an email to that topic. 😊)
-
Use a callback.
func grep(r io.Reader, pattern string, final func(error)) (iter.Seq2[int64, string], error) {In this case, errDB is called after the iterator completes, with whatever value is returned by
scanner.Err()(possiblynil):func grep(r io.Reader, pattern string, final func(error)) (iter.Seq2[int64, string], error) { re, err := regexp.Compile(pattern) if err != nil { return nil, err } var lineNumber int64 scanner := bufio.NewScanner(r) return func(yield func(string) bool) { lineNumber++ for scanner.Scan() { line := scanner.Text() if re.MatchString(line) { if !yield(lineNumber, line) { break } } } final(scanner.Err()) }, nil }
Which approach should you use? Well, it’s hard to offer a blanket rule, because there are many different scenarios. But personally, I tend to reach for the Result struct pattern first. I would very rarely use the Err() method approach, just because of the added overhead it requires in setting up new types. I do use the callback approach occasionally, but usually when there’s other metadata to return (like pagination data), not just for an error.
Which pattern do you like best? Are there any I’ve overlooked?