分页是 Web 开发的标配功能。数据量大时,不可能一次性加载全部,分页展示既节省资源又提升用户体验。
Offset + Limit 实现:
page := 1
pageSize := 10
var users []User
db.Offset((page - 1) * pageSize).Limit(pageSize).Find(&users)
分页通常需要显示总条数和总页数:
var users []User
var total int64
db.Model(&User{}).Count(&total)
page := 1
pageSize := 10
db.Offset((page - 1) * pageSize).Limit(pageSize).Find(&users)
totalPages := (total + int64(pageSize) - 1) / int64(pageSize)
封装分页结果:
type Page[T any] struct {
List []T `json:"list"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
TotalPages int `json:"total_pages"`
}
func NewPage[T any](list []T, total int64, page, pageSize int) *Page[T] {
totalPages := int(total) / pageSize
if int(total)%pageSize > 0 {
totalPages++
}
return &Page[T]{
List: list,
Total: total,
Page: page,
PageSize: pageSize,
TotalPages: totalPages,
}
}
通用分页函数:
func Paginate[T any](db *gorm.DB, page, pageSize int) (*Page[T], error) {
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 10
}
if pageSize > 100 {
pageSize = 100
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, err
}
var list []T
offset := (page - 1) * pageSize
if err := db.Offset(offset).Limit(pageSize).Find(&list).Error; err != nil {
return nil, err
}
return NewPage(list, total, page, pageSize), nil
}
var users []User
result, err := Paginate[User](db.Model(&User{}).Where("status = ?", "active"), 1, 10)
func GetUserPage(db *gorm.DB, params QueryParams) (*Page[User], error) {
query := db.Model(&User{})
if params.Name != "" {
query = query.Where("name LIKE ?", "%"+params.Name+"%")
}
if params.Status != "" {
query = query.Where("status = ?", params.Status)
}
return Paginate[User](query.Order("id desc"), params.Page, params.PageSize)
}
传统分页在大偏移量时性能差,游标分页是更好的选择:
type CursorPage[T any] struct {
List []T `json:"list"`
NextID int64 `json:"next_id"`
HasMore bool `json:"has_more"`
}
func CursorPaginate[T any](db *gorm.DB, lastID int64, pageSize int) (*CursorPage[T], error) {
var list []T
query := db.Limit(pageSize + 1)
if lastID > 0 {
query = query.Where("id > ?", lastID)
}
if err := query.Order("id asc").Find(&list).Error; err != nil {
return nil, err
}
hasMore := len(list) > pageSize
if hasMore {
list = list[:pageSize]
}
var nextID int64
if len(list) > 0 {
nextID = reflect.ValueOf(list[len(list)-1]).FieldByName("ID").Int()
}
return &CursorPage[T]{
List: list,
NextID: nextID,
HasMore: hasMore,
}, nil
}
移动端常用无限滚动,本质是游标分页:
func LoadMore(db *gorm.DB, lastID int64, pageSize int) ([]User, bool, error) {
var users []User
query := db.Limit(pageSize + 1)
if lastID > 0 {
query = query.Where("id < ?", lastID)
}
if err := query.Order("id desc").Find(&users).Error; err != nil {
return nil, false, err
}
hasMore := len(users) > pageSize
if hasMore {
users = users[:pageSize]
}
return users, hasMore, nil
}
避免 Count 查询
Count 查询在大表上很慢。可以考虑:
延迟关联
大偏移量时,先查 ID 再关联:
var ids []int64
db.Model(&User{}).Offset(100000).Limit(10).Pluck("id", &ids)
var users []User
db.Where("id IN ?", ids).Find(&users)
使用覆盖索引
确保排序字段有索引,查询只走索引:
db.Select("id, name, age").Order("id").Offset(100000).Limit(10).Find(&users)
分页参数通常从请求中获取:
func GetUsers(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
result, err := Paginate[User](db.Model(&User{}), page, pageSize)
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
c.JSON(200, result)
}
分页是基础但重要的功能。传统分页简单通用,游标分页性能更好。根据场景选择合适的方式,注意大偏移量问题。