Implementing iterators

October 16, 2025

The last few weeks I’ve been talking about different iterator patterns, but from the perspective of consuming iterators. Let’s switch angles now, and begin talking about how to implement iterators.

To illustrate, let’s use a simple example of a grep utility. It will read an io.Reader, and return any lines that match a regular expression input. Here’s a simple implementation, that returns all results at once (no iteration):

func grep(r io.Reader, pattern string) ([]string, error) {
  re, err := regexp.Parse(pattern)
  if err != nil {
    return nil, err
  }
  scanner := bufio.NewScanner(r)
  var matches []string
  for scanner.Scan() {
    line := scanner.Text()
    if re.MatchString(line) {
      matches = append(matches, line)
    }
  }
  if err := scanner.Err(); err != nil {
    return nil, err
  }
  return matches, nil
}

And as expected, this works fine for small inputs, but would not be ideal for large inputs, or if you want to stop processing after the first N results, for example… this is where iterators are quite useful, and we’ll be looking at re-implementing this using different iterator patterns for the rest of this series.


Share this

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

Unsure? Browse the archive .

Related Content


Implementing a channel-based iterator

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.


Errors with range over func

Last week I introduced the topic of range-over-func as an iterator pattern. Let’s look at how error handling differs with this approach. For context, let’s compare the custom iterator approach, which has three distinct errors to check: 1. The initial function call, 2. while iterating over each row, 3. finally at the end, to ensure iteration completed successfully. orders, err := db.Orders(ctx, userID) if err != nil { return err } defer orders.


Range over func

Since Go 1.23, we’ve had a new way we can implement iterators. I’ve written previously about it if you’re interested. But today we’ll take a glance at how it affects our Orders example: orders, err := db.Orders(ctx, userID) if err != nil { return err } for order, err := range orders { if err != nil { return err } /* Do something with each order */ } Unless you’re already quite familiar with the range-over-func feature, it’s probably not immediately clear what’s going on here, just by looking at the code.

Get daily content like this in your inbox!

Subscribe