Iterator callbacks

November 19, 2025

Today I want to expand with a thought I touched on yesterday: Callbacks with iterators.

Yesterday’s context was error handling. But I’ve found callbacks with iterators to be very valuable in a slightly different case. To understand the problem callbacks have helped me solve, we need to move away from our grep example to something a bit more involved. Let’s imagine we’re querying a REST API, which returns paginated results. A typical response body might look something like this, in JSON:

{
  "items": [
    {
      "id": 1234,
      "description": "Foo",
      ...
    },
    {
      "id": 2345,
      "description": "Bar",
      ...
    },
    ...
  ],
  "page": {
    "page_no": 2,
    "total_items": 1346,
    "items_per_page": 20,
    "cursor": "Y3Vyc29yCg=="
  }
}

Now you might just read the entire response body in at once, parse it, then return a slice of items, along with some pagination data:

func (c *client) Items(ctx context.Context, /* ... */) ([]*Item, *Page, error)

But if these items are particularly large–maybe they contain Base64-encoded images, or videos, for example. Or maybe we’re returning pages of 1,000 items at a time. Or for whatever reason, you want to parse them one at a time, as they come in over the network, rather than all at once. Now we want an iterator:

func (c *client) Items(ctx context.Context, /* ... */) iter.Seq2[*Item, error]

One problem solved nicely, but what about that pagination data? It likely cannot even be read off the network until the entire rest of the response has been read and parsed. So we need some way read and return that data to the user after processing has completed.

This is where I find a callback very useful:

func (c *client) Items(ctx context.Context, final func(*Page), /* ... */) iter.Seq2[*Item, error]

In this case, the Items method can call the final callback once iteration has completed successfully. And unlike the approach of adding a Result object with a Page method on it, there’s no possible way for the consumer to try to read the pagination data too early.

I used this technique in my Ticketmaster client library, which you’re welcome to look at for specifics. (Here I mixed this technique with functional options—a topic I should perhaps address another day.)


Share this

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

Unsure? Browse the archive .

Related Content


Alternatives to iter.Seq3

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:


Handling errors during iteration with range-over-func

Yesterday we looked at a range-over-func iterator that was missing a vital piece: Error handling during iteration. I know of three possible solutions to this problem, and today we’ll look at the simplest of them, which I typically recommend: Using iter.Seq2. We’ve already looked at this pattern from the consumer’s perspective. Today we’ll see how the implementation works. func grep(r io.Reader, pattern string) (iter.Seq2[string, error], error) { re, err := regexp.


Implementing a range-over-func iterator

It’s finally time to look at how we implement a range-over-func iterator. I’d venture a guess this should be the go-to pattern for most iterators, unless or until you have special needs that it won’t address. And, of course, we’ll discuss some of those in the near future, as well. First, here’s how our grep implementation looks using range-over-func: func grep(r io.Reader, pattern string) (iter.Seq[string], error) { re, err := regexp.

Get daily content like this in your inbox!

Subscribe