Open4

aws-sdk-go-v2のページングがあるAPIをGo 1.23のイテレーターで実装する

fujiwarafujiwara

例えば ecs.ListTasks をイテレーターにするにはこんな感じのコードになる。

func ecsNewListTasksIterator(ctx context.Context, c *ecs.Client, in *ecs.ListTasksInput) func(func(*ecs.ListTasksOutput, error) bool) {
	return func(yield func(*ecs.ListTasksOutput, error) bool) {
		for {
			out, err := c.ListTasks(ctx, in)
			if err != nil {
				yield(nil, err)
				return
			}
			if !yield(out, err) || out.NextToken == nil {
				return
			}
			in.NextToken = out.NextToken
		}
	}
}

使う側はこう

client := ecs.NewFromConfig(awsCfg)
for out, err := range ecsNewListTasksIterator(ctx, client, input) {
	if err != nil {
		return err
	}
	// ... out を使って何かする
}
fujiwarafujiwara

Genericsを使って一般化するとこう

func newAnyIterator[IN any, OUT any, OPT any](ctx context.Context, do func(context.Context, *IN, ...OPT) (*OUT, error), next func(*IN, *OUT) bool, in *IN, opt ...OPT) func(func(*OUT, error) bool) {
	return func(yield func(*OUT, error) bool) {
		for {
			out, err := do(ctx, in, opt...)
			if err != nil {
				yield(nil, err)
				return
			}
			if !yield(out, err) {
				return
			}
			if !next(in, out) { // if next returns true, has a next page. continue
				return
			}
		}
	}
}
  • do = aws-sdk-go-v2 のあらゆる client のメソッドとおなじ interface の関数
  • next = output から次のページがあるかを判定し、あればinputにnextTokenを設定して true を返す関数
    • ここはAPIごとにtokenの名前が違ったりするので個別に書くしかない
    • あるいはreflectで頑張るか…?

でこれを使うとこう書ける

// next の実装
func ecsListTasksNext(in *ecs.ListTasksInput, out *ecs.ListTasksOutput) bool {
	if out.NextToken == nil {
		return false
	}
	in.NextToken = out.NextToken
	return true
}

for res, err := range newAnyIterator(ctx, client.ListTasks, ecsListTasksNext, input) {
	// ... 
fujiwarafujiwara

(参考)
もともと sdk v2 には Paginator という仕組みがあり ecs#ListTasksPaginator

こんな風に書ける。

p := ecs.NewListTasksPaginator(client, input)
for p.HasMorePages() {
	out, err := p.NextPage(ctx)
	// ...
}

イテレーターにすると、HasMorePagesとNextPageという2つの関数を呼ぶ必要がなくて普通に range でループをまわせばよいのでちょっとだけ分かりやすい。SDK でもイテレータを提供してくれないかな? (v3かなあ…)