Áttekintés
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.
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.
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, aCursorPage[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.