Iterator patterns

September 29, 2025

I’m going to change gears from the previous discussion about goroutines, to different ways of iterating. This can closely relate to goroutines, so we’ll bounce back and forth a bit, I suspect. Meanwhile, if you have any questions specifically about goroutines that I didn’t cover, send me an email. I’ll be happy to fill in those gaps!


First off, before really diving into iterator patterns, let’s talk about why we might want iterators. This will help frame the larger discussion.

string := "Hello, world!"
str := "Hello, world!"
for _, char := range str {
	fmt.Println(char)
}

This code loops over each character of the string Hello, world!, and prints the ASCII value of each character, one per line:

72
101
108
108
...

And, of course, we use loops similar to this (though typically more interesting) all the time. That is, we frequently need to iterate over a number of items. And the simplest way to do this is often with a simple for loop, as illustrated.

And for such a small string, this approach is fine. But what if our input were much longer? Perhaps megabytes or gigabytes in size? And maybe we’re doing something more involved that simply printing each character in turn? Maybe the thing we’re doing involves a non-trivial amount of calculation, or consumes a lot of memory, or involves spawning multiple goroutines…

Depending on these, and other factors, we often want something more sophisticated than a simple for loop.

What other options do we have? Of course we’ll be diving into the details, but here’s a preview:

  • index-based for loops
  • channels
  • range-over-func for loops
  • custom iterators

Share this

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

Unsure? Browse the archive .

Related Content


Iterating over channels

For this discussion of iterators, let’s establish a baseline example. It’s made up, but realistic and common: A database method that returns all user orders. We’ll be experimenting with different function signatures, but in general, this is what we can imagine it will look like: func (DB) Orders(ctx context.Context, userID string) ([]*Order, error) And we would consume it with code something like this: orders, err := db.Orders(ctx, userID) if err !


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