paginate

Áttekintés

Offset- (page/per-page) lapozás a paginate modullal, normalizált paraméterek és egy típusos page-wrapper.

A paginate egy önálló Go modul (github.com/gp-system/paginate), ami a lapozás két unalmas-de-veszélyes felét központosítja: a kliensinput normalizálását (határok, defaultok, túlcsordulás-védelem) és az eredmény becsomagolását a válaszmodellhez. Két módot ad: a klasszikus page/per-page (offset) lapozást, amit ez az oldal tárgyal, és a cursor- (keyset-) lapozást, aminek saját oldala van, mert más a paraméter-alakja és más a helyességi szabályok köre.

import "github.com/gp-system/paginate"

Telepítés

go get github.com/gp-system/paginate@v0.1.0 # a kit is ezt a taget használja
go get github.com/gp-system/paginate@latest

Go 1.25+. A paginate csak az errs-től függ (az ErrInvalidCursor miatt); nincs query builder, nincs adatbázis-driver, más nem kerül be vele.

Offset lapozás

A generált request-paraméterek opcionális pointerként érkeznek (*int); a Normalize egyszer szorítja őket határok közé, nem minden handlerben külön:

func Normalize(page, perPage *int, opts ...Option) Params

type Params struct{ Page, PerPage int }
func (p Params) Limit() int  // SQL LIMIT
func (p Params) Offset() int // SQL OFFSET: (Page-1)*PerPage
p := paginate.Normalize(req.Params.Page, req.Params.PerPage) // defaultok: page 1, perPage 20
rows, err := r.db.Query(ctx,
    `SELECT ... ORDER BY created_at DESC LIMIT $1 OFFSET $2`, p.Limit(), p.Offset())

A határok opciókkal hangolhatók:

p := paginate.Normalize(page, perPage,
    paginate.WithDefaultPerPage(15), // ha a kérés nem ad perPage-et (default: 20)
    paginate.WithMaxPerPage(50),     // felső sapka, bármit is kér a kliens (default: 100)
    paginate.WithMaxPage(10_000))    // a page number sapkája (default: 1 000 000)

A WithMaxPage nem kozmetika: az Offset() a (page-1)*perPage szorzat. Sapka nélkül egy ellenséges ?page= érték int-túlcsordulást vagy patologikus OFFSET-et provokálhatna. A Normalize után az Offset() garantáltan biztonságos.

Az eredményt a Page[T] csomagolja a válaszhoz:

func NewPage[T any](items []T, total int64, p Params) Page[T]

type Page[T any] struct {
    Items   []T
    Total   int64
    Page    int
    PerPage int
}

A Page[T] a scaffoldolt shared/paginator.tsp Paginated<T> modelljének tükre, így a generált válaszmodellre mappelés mechanikus.

Az offset lapozás a kényelmes default kis, admin-jellegű listákhoz, ahol a Total és az adott oldalra ugrás jobban számít, mint a nyers áteresztőképesség. Egy nagy vagy gyorsan növő táblán a cursor-lapozás az, ami gyors és stabil marad, ahogy a tábla nő; a teljes összevetéshez lásd Mikor melyiket.

Önálló példa

A paginate-hez semmi nem kell a kitből: egy sima net/http handler elég a normalizálj-majd-csomagold alak bemutatásához.

main.go
package main

import (
    "encoding/json"
    "net/http"
    "strconv"

    "github.com/gp-system/paginate"
)

type Product struct {
    ID   string `json:"id"`
    Name string `json:"name"`
}

func listProducts(w http.ResponseWriter, r *http.Request) {
    page := atoiPtr(r.URL.Query().Get("page"))
    perPage := atoiPtr(r.URL.Query().Get("per_page"))
    p := paginate.Normalize(page, perPage, paginate.WithMaxPerPage(50))

    items, total := fetchProducts(p.Limit(), p.Offset()) // saját query
    json.NewEncoder(w).Encode(paginate.NewPage(items, total, p))
}

func atoiPtr(s string) *int {
    if s == "" {
        return nil
    }
    n, err := strconv.Atoi(s)
    if err != nil {
        return nil
    }
    return &n
}

func fetchProducts(limit, offset int) ([]Product, int64) {
    return nil, 0 // saját repository-hívás
}

A SQL a tiéd

Fontos tulajdonság: a paginate nem rejti el a lapozást egy query builder mögé: itt a SQL a tiéd, a modul csak a paraméter-fegyelmet és a lapmechanikát adja (paginate.Normalize a bemenethez, paginate.NewPage a válaszhoz). Cserébe a LIMIT/OFFSET (vagy cursoroknál a seek-predikátum és az indexe) a te kezedben van.

Használt patternek

  • Generikus eredmény-wrapper (Page[T]): a Design patternek oldal a cursor-testvérét, a CursorPage[T]-t tárgyalja mélységében.
  • Functional options (a With* határ-hangolás): Design patternek.

Merre tovább

  • Cursor-lapozás: CursorParams, NormalizeCursor, CursorPage[T], a seek-predikátum, és mikor éri meg offset helyett.
Copyright © 2026