最近看到一篇有关GO泛型使用,对比最近使用Java编写的后端,感觉还是有点差别,毕竟是个人的感受。以前博客也有一篇有关go的文章,有兴趣的可以浏览《详解泛型是如何让你的GO代码变慢》。

功能假设

假设我们有一个用于总账的 RESTful API,带有端点,它返回一个分页的资源集合:

  1. GET /accounts,检索帐户列表,按一些查询参数过滤和排序;
  2. GET /accounts/:uuid/transactions,检索帐户的交易列表;
  3. GET /postings,检索存储在分类帐中的过帐列表。

实际结构和响应体是实现细节,在这里并不重要。

功能实现

在 Go 1.18 中,我们可以将此 API 的客户端方法指定为相应的List*方法,返回泛型类型Pager[T any]

type LedgerAPIClient struct {
    // unexported fields
}

func (c *LedgerAPIClient) ListAccounts(context.Context, ListAccountsInput) *Pager[Account] { ··· }

func (c *LedgerAPIClient) ListAccountTransactions(context.Context, ListAccountTransactionsInput) *Pager[Transaction] { ··· }

func (c *LedgerAPIClient) ListPostings(context.Context, ListPostingsInput) *Pager[Posting] { ··· }

泛型类型的 APIPager[T any]如下所示:

// PagerDone is a sentinel error, which pager's NextPage and PrevPage methods return,
// when there aren't any more pages to iterate over.
var PagerDone = errors.New("no more pages to iterate")

type Pager[T any] struct {
    // unexported fields
}

// NextPage retrieves the next page in the collection of T.
// It returns PagerDone error after it reaches the last page.
func (p *Pager[T]) NextPage() ([]T, error) { ··· }

// PrevPage retrieves the previous page in the collection of T.
// It returns PagerDone error after it reaches the first page.
func (p *Pager[T]) PrevPage() ([]T, error) { ··· }

type PageInfo struct {
    Token   string
    HasNext bool
    HasPrev bool
}

// PageInfo returns a metadata about the current page.
func (p *Pager[T]) PageInfo() PageInfo { ··· }

在底层,该Pager[T]类型实现了一个迭代器,它将状态与当前页面保持一致,并通过对其 API 客户端的反向引用调用 API 端点,以检索具有下一页或上一页的列表。实际实现可能如本示例 gist所示。

为了遍历资源的集合,我们的用户LedgerAPIClient调用List*相关API资源的方法,遍历返回的pager,直到后者用完,或者用户对手头的结果满意:

c := api.NewLedgerAPIClient()

pager := c.ListAccounts(ctx, input)
for {
    accs, err := pager.NextPage()
    if errors.Is(err, api.PagerDone) {
        break
    }
    if err != nil { ··· }

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

比方说,对于某个用例,使用集合更方便,就好像它不是一个分页的资源列表,而是一个连续的个人资源流。

这个实现的好处是泛型类型Pager[T]实现了一个泛型接口:

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

我们可以实现一个帮助器,它接受一个pageIterator[T any]接口,并使用它的NextPage()方法,帮助器扫描整个集合,一个一个地返回它的项目。泛型允许实现这个助手一次,它可以在我们的 API 客户端中使用任何类型的资源,并为用户提供一个强类型的 API 来使用:

type PageScanner[T any] struct {
    Pager pageIterator[T]
    // a buffer of items yet to be conumed by the user
    list [T]
}

// Next returns next item from the underlying pager.
// After all items of the current page are consumed, PageScanner moves to the next page.
func (s *PageScanner[T any]) Next() (item T, err error) {
    for len(s.list) == 0 {
        s.list, err = s.Pager.NextPage()
        if err != nil {
            return item, err
        }
    }

    item = s.list[0]
    s.list = s.list[1:]

    return item, nil
}

这就是用户将其应用于他们的代码的方式:

c := api.NewLedgerAPIClient()

pager := c.ListAccountTransactions(ctx, input)

// (caveat) it seems that, in Go 1.18 the compiler can't figure out the underlying type
// of the pager's list, so we have to specify the type explicitely, when create PageScanner[T]
scanner := &api.PageScaner[Transaction]{
    Pager: pager,
}
for {
    trx, err := scanner.Next()
    if errors.Is(err, api.PagerDone) {
        break
    }
    if err != nil { ··· }

    // do something with an individual transaction
}