最近看到一篇有关GO的泛型使用,对比最近使用Java编写的后端,感觉还是有点差别,毕竟是个人的感受。以前博客也有一篇有关go的文章,有兴趣的可以浏览《详解泛型是如何让你的GO代码变慢》。
功能假设
假设我们有一个用于总账的 RESTful API,带有端点,它返回一个分页的资源集合:
GET /accounts
,检索帐户列表,按一些查询参数过滤和排序;GET /accounts/:uuid/transactions
,检索帐户的交易列表;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] { ··· }
// 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
}
📮评论