Today let’s look at re-implementing our non-iterating grep function using a channel for iteration.
First the code:
func grep(r io.Reader, pattern string) (chan <- string, error) {
re, err := regexp.Parse(pattern)
if err != nil {
return nil, err
}
matches := make(chan string)
go func() {
defer close(matches)
scanner := bufio.NewScanner(r)
var matches []string
for scanner.Scan() {
line := scanner.Text()
if re.MatchString(line) {
matches <- line
}
}
if err := scanner.Err(); err != nil {
panic(err)
}
}()
return matches, nil
}
So this code “works”. It allows us to read results from grep() as they are encountered, rather than waiting to process the entire input first. So that alone is a bit of an improvement. But this code contains a few other problems as well.
For today we’ll consider one of them. Perhaps most obvious, it includes a panic() call when an error occurs after returning the channel. Not at all ideal. There are a couple main ways this could be improved:
- Rather than returning a raw channel, return an object that exposes both the channel for iteration, and a final error.
type GrepResult struct {
matches chan <- string
err error
}
func (r *GrepResult) Matches() chan <- string {
return r.matches
}
func (r *GrepResult) Err() error {
return r.err
}
func grep(r io.Reader, pattern string) (*GrepResult, error) {
re, err := regexp.Parse(pattern)
if err != nil {
return nil, err
}
result := &GrepResult{
matches: make(chan string),
}
go func() {
defer close(matches)
scanner := bufio.NewScanner(r)
var matches []string
for scanner.Scan() {
line := scanner.Text()
if re.MatchString(line) {
result.matches <- line
}
}
if err := scanner.Err(); err != nil {
result.err = err
}
}()
return result, nil
}
This makes using the iterator a bit more cumbersome, but still possible:
result, err := grep(file, pattern)
if err != nil {
return err
}
for match := range result.Matches() { /* do something */ }
if err := match.Err(); err != nil {
return err
}
We could simplify slightly by deferring the first error to the reesult:
func grep(r io.Reader, pattern string) (*GrepResult) {
re, err := regexp.Parse(pattern)
if err != nil {
return &GrepResult{err: err}
}
/* the rest is the same */
Then using it is simplified to:
result := grep(file, pattern)
for match := range result.Matches() { /* do something */ }
if err := match.Err(); err != nil {
return err
}
The other option would be to modify the result channel to return a struct with two values: The matching line, and an error. This approach makes more sense in cases where you may have one error per result, rather than a single error that terminates iteration, so probably not ideal in this case, but it is an option to consider.
type Result struct {
Match string
Err error
}
func grep(r io.Reader, pattern string) (chan <- Result, error) {
re, err := regexp.Parse(pattern)
if err != nil {
return nil, err
}
matches := make(chan Result)
go func() {
defer close(matches)
scanner := bufio.NewScanner(r)
var matches []string
for scanner.Scan() {
line := scanner.Text()
if re.MatchString(line) {
matches <- Result{Line: line}
}
}
if err := scanner.Err(); err != nil {
matches := Result{Err: err}
}
}()
return matches, nil
}