Implementing a channel-based iterator

October 17, 2025

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:

  1. 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
}

Share this

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

Unsure? Browse the archive .

Get daily content like this in your inbox!

Subscribe