Client-side pagination in Go (range-over function edition)

In light of Go 1.22’s experiment, that allows iterating over a function, I wanted to revisit my note from 2022 on client-side pagination, using a generic iterator, and get an idea on how the range-over function might help with the task. I will not delve into the details of the changes that Go brings. Refer to the “Rangefunc Experiment” on the Go wiki for more details. One only thing to mention, is that the executable must be built with the GOEXPERIMENT=rangefunc environment variable, to enable the experiment.

First, let’s recap where the previous note landed. We have a client for an API, whose List* methods return a generic iterator type Pager[T]:

package api

type LedgerAPIClient { ··· }

func (*LedgerAPIClient) ListAccounts(···) *Pager[Account]

Users traverse through paginated lists of T using the Pager[T].NextPage() method:

client := api.NewLedgerAPIClient()

pager := client.ListAccounts(···)
for {
    accs, err := pager.NextPage()
    if errors.Is(err, api.PagerDone) {
        // PagerDone is sentinel error
        break
    }
    if err != nil { ··· }

    for _, acc := range accs {
        // do something with this page's list of accounts
    }
}

The “real life use-case for generics in Go: API for client-side pagination” note explains the details of the API; I won’t repeat them here.


We implement an adaptor function, that converts our Pager[T] iterator into a function iterator:

package pages

// "iter" package is new in Go 1.22.
// It's available only under `GOEXPERIMENT=rangefunc`
import "iter"

type pageIter[T any] interface {
    NextPage() ([]T, error)
}

// Pages accepts pagerIter and returns an iterator over sequences of pairs of []T and error.
// The returned iterator stops on first occurrence of PagerDone sentinel error, that p must return.
func Pages[T any](p pageIter[T]) iter.Seq2[[]T, error] {
    return func(yield func([]T, error) bool) {
        for {
            list, err := p.NextPage()
            if errors.Is(err, PagerDone) || !yield(list, err) {
                return
            }
        }
    }
}

This is how users apply this Pages iterator function to their code:

client := api.NewLedgerAPIClient()

pager := client.ListAccounts(···)

// iterate over pages of account lists
for accs, err := range pages.Pages(pager) {
    if err != nil { ··· }

    for _, acc := range accs {
        // do something with the page's list of accounts
    }
}

Let’s go further and implement another helper, that allows user to iterate over the whole collection as a continues list of entities, instead of traversing them page-by-page:

package pages

// Scan convers an iterator over sequences of pairs of []T and error into an iterafor
// over sequences of pairs of T, error.
func Scan[T any](ps iter.Seq2[[]T, error]) iter.Seq2[T, error] {
    return func(yield func(T, error) bool) {
        for list, err := range ps {
            if err != nil {
                var zero T
                if !yield(zero, err) {
                    return
                }
            }
            for _, v := range list {
                if !yield(v, nil) {
                    return
                }
            }
        }
    }
}

When applying the Scan iterator function, this is what users’ code would look like:

client := api.NewLedgerAPIClient()

pager := client.ListAccounts(···)

// iterate over accounts
for acc, err := pages.Scan(pages.Pages(pager)) {
    if err != nil { ··· }

    // do something with individual account
}

It will, for sure, be fascinating to observe how a standardized iterators will impact the trends in the APIs of Go packages. Currently, I’m not very comfortable about the function-that-returns-function-that-takes-function API, and the “pyramid of doom”, that the code of these function iterators introduces, when we start fusing and chaining iterators. However, as is often with new language concepts, the range-over function iterators, if promoted out of the experiment, will require some time, exploration, and getting used to.

What do you think about that? Share your thoughts on Bluesky, Twitter and Reddit.

All topics

applearduinoarm64askmeawsberlinbookmarksbuildkitcgocoffeecontinuous profilingCOVID-19designdockerdynamodbe-paperenglishenumesp8266firefoxgithub actionsGogooglegraphqlhomelabIPv6k3skuberneteslinuxmacosmaterial designmDNSmusicndppdneondatabaseobjective-cpasskeyspostgresqlpprofprofeferandomraspberry pirusttravis civs codewaveshareµ-benchmarks