Go语言中的客户端分页(range-over 函数版)

Go 1.22 实验版在允许在一个函数上进行迭代,有鉴于此,我想重温一下我在 2022 年写的关于使用泛型迭代器进行客户端分页的笔记,并了解 range-over 函数如何帮助完成这项任务。我不会深入探讨 Go 带来的变化的细节。更多细节请参阅 Go wiki 上的 “Rangefunc 实验“。唯一需要提及的是,要启用该实验,必须在构建可执行文件时使用 GOEXPERIMENT=rangefunc 环境变量。

首先,让我们回顾一下前面的说明。我们有一个应用程序接口的客户端,其 List* 方法会返回一个通用迭代器类型 Pager[T]

package api

type LedgerAPIClient { ··· }

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

用户使用 Pager[T].NextPage() 方法遍历 T 的分页列表:

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

“泛型在 Go 中的实际应用案例:用于客户端分页的 API “注释解释了该 API 的细节,在此不再赘述。


我们实现了一个适配器函数,将 Pager[T] iterator 转换为函数 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
            }
        }
    }
}

这就是用户如何将此 Pages迭代器函数应用到他们的代码中:

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

让我们更进一步,实现另一个辅助工具,它允许用户以实体的持续列表形式遍历整个集合,而不是逐页遍历:

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

应用 Scan 迭代器函数时,用户的代码会是这样的:

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
}

当然,观察标准化的迭代器将如何影响 Go 软件包 API 的发展趋势将是一件令人着迷的事情。目前,我对 “返回函数”(function-that-returns)和 “获取函数”(function-that-takes-function)的 API 以及 “厄运金字塔”(pyramid of doom)还不是很放心,因为当我们开始融合和连锁迭代器时,这些函数迭代器的代码会引入厄运金字塔。不过,就像新的语言概念一样,迭代函数的范围如果从实验中推广出来,也需要一些时间、探索和适应。

本文文字及图片出自 Client-side pagination in Go (range-over function edition)

阅读余下内容
 

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注


京ICP备12002735号