実装

package util

// Paginator paginator
type Paginator struct {
	Limit int
	Loop  int
}

const defaultLimit = 50

// NewDefaultPaginator return Paginator
func NewDefaultPaginator() *Paginator {
	return &Paginator{
		Limit: defaultLimit,
	}
}

// Pagination ページネーション
func (p *Paginator) Pagination(next func(p *Paginator) (bool, error)) error {
	for {
		isContinue, err := next(p)
		if err != nil {
			return err
		}
		if !isContinue {
			break
		}
		p.Loop++
	}
	return nil
}

// IsBreak 総数から break 条件に達しているかチェック
func (p *Paginator) IsBreak(total int64) bool {
	return p.Loop == int((total-1)/int64(p.Limit))+1
}

// Offset ページオフセット
func (p *Paginator) Offset() int {
	return p.Loop * p.Limit
}

うさげ(テスト)

package util_test

import (
	"fmt"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/voicyworld/total-sales-batch/internal/util"
)

func TestPagination(t *testing.T) {
	p := util.NewDefaultPaginator()

	num := 164

	// 期待値 [1, 2, 3 ... 50, 1, 2, 3 ... 50 ...]
	var expected []int
	for i := 0; i < num; i++ {
		expected = append(expected, i%50+1)
	}

	actualLoopCount := 0
	actual := make([]int, num)
	err := p.Pagination(func(p *util.Paginator) (bool, error) {
		if p.IsBreak(int64(num)) {
			return false, nil
		}

		max := p.Limit * (actualLoopCount + 1)
		min := max - p.Limit
		for i := 1; i <= p.Limit; i++ {
			index := min + (i - 1)
			if len(actual) <= index {
				break
			}
			actual[index] = i
		}

		actualLoopCount++

		return true, nil
	})

	assert.Nil(t, err)
	assert.Equal(t, expected, actual)
	assert.Equal(t, 4, actualLoopCount)
}

func TestIsBreak(t *testing.T) {
	t.Parallel()

	tests := []struct {
		loop     int
		limit    int
		total    int64
		expected bool
	}{
		{0, 50, 1, false},
		{1, 50, 1, true},
		{0, 50, 50, false},
		{1, 50, 50, true},
		{1, 50, 51, false},
		{2, 50, 51, true},
	}
	for i, tt := range tests {
		t.Run(fmt.Sprint("case_", i), func(t *testing.T) {
			p := &util.Paginator{
				Limit: tt.limit,
				Loop:  tt.loop,
			}
			actual := p.IsBreak(tt.total)
			assert.Equal(t, tt.expected, actual)
		})
	}
}

func TestOffset(t *testing.T) {
	t.Parallel()

	tests := []struct {
		limit    int
		loop     int
		expected int
	}{
    {50, 0, 0},
		{50, 1, 50},
		{50, 2, 100},
	}
	for i, tt := range tests {
		t.Run(fmt.Sprint("case_", i), func(t *testing.T) {
			p := &util.Paginator{
				Limit: tt.limit,
				Loop:  tt.loop,
			}
			actual := p.Offset()
			assert.Equal(t, tt.expected, actual)
		})
	}
}