GORM分页唯一可靠方式是Limit+Offset组合,因GORM无原生Paginate;Offset必须为(page-1)*page_size且page≥1,需校验参数、显式Order排序、独立执行Count查询。

为什么直接用 Limit 和 Offset 在 Fiber 里分页容易出问题
因为 Fiber 本身不内置分页逻辑,Limit 和 Offset 是数据库层面的操作,而用户传的 page、page_size 需要手动校验和转换——稍不注意就会触发 SQL 注入、越界查询或空页崩溃。比如传 page=0 或 page_size=-100,没拦截就直接进 db.Offset(),Go 的 int 溢出或数据库报错都可能发生。
实际开发中更常见的坑是:前端传 page=1,后端按“从第 0 条开始”算偏移量,但忘了把 page 转成 offset = (page - 1) * page_size,结果第一页数据永远少一屏。
- 必须对
page和page_size做白名单校验(比如限定page >= 1、page_size在 1–100 之间) - 建议统一用
uint64接收分页参数,避免负数转成大正数(Go 中int(-1)转uint64变成 18446744073709551615) - 不要信任 Query 参数,
c.Query("page")返回的是字符串,必须显式strconv.ParseUint并检查err
怎么用 GORM + Fiber 写一个安全的分页 handler
假设你用 GORM 当 ORM,核心是把分页参数转成 Limit/Offset,再加总数统计。关键不是“怎么查”,而是“怎么不让非法参数进 DB”。
示例 handler:
func ListPosts(c *fiber.Ctx) error {
page, err := strconv.ParseUint(c.Query("page", "1"), 10, 64)
if err != nil || page < 1 {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid page"})
}
pageSize, err := strconv.ParseUint(c.Query("page_size", "10"), 10, 64)
if err != nil || pageSize < 1 || pageSize > 100 {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid page_size"})
}
<pre class="brush:php;toolbar:false;">offset := (page - 1) * pageSize
var posts []model.Post
var total int64
// 一次查总数(避免 COUNT(*) 全表扫,可考虑缓存或估算)
if err := db.Model(&model.Post{}).Count(&total).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "count failed"})
}
// 再查数据
if err := db.Limit(int(pageSize)).Offset(int(offset)).Find(&posts).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "query failed"})
}
return c.JSON(fiber.Map{
"data": posts,
"total": total,
"page": page,
"page_size": pageSize,
})}
注意:db.Limit() 和 db.Offset() 参数是 int,所以必须显式转类型;GORM v2 默认不自动处理分页字段名,别指望它识别 page 参数。
Cursor 分页在 Fiber 里怎么落地(替代 offset)
当数据量上百万、且有频繁插入时,OFFSET 会越来越慢。这时该切到 cursor 分页:用上一页最后一条记录的排序字段值(如 id 或 created_at)作为下一页起点。
Fiber 层只需透传 cursor 字符串,后端解码并拼进 WHERE 条件:
- 前端传
?cursor=MTIzNA==(base64 编码的上一页末条id) - 后端用
base64.StdEncoding.DecodeString解出原始id,再做WHERE id > ? - 必须强制要求排序字段有索引,且不能用
ORDER BY RAND()这类非确定性排序 - 首次请求无 cursor,就默认查第一页(
ORDER BY id DESC LIMIT 10)
这种模式下,Fiber 不需要改路由或中间件,只换参数解析逻辑和 SQL 构建方式。但要注意:游标值必须唯一、有序、不可变——用 created_at 要小心并发写入导致时间重复。
分页响应结构要不要包含 next/prev 链接
RESTful API 里带链接(HATEOAS 风格)看着规范,但在 Fiber 实现里基本是徒增复杂度。生成 next 链接要重拼当前所有 query 参数,还要处理 cursor 编码、URL 转义;而前端其实只需要 total 和当前 page/page_size 就能自己算出下一页 URL。
更现实的做法是:只返回 data、total、page、page_size 四个字段。如果真要链接,也只在调试环境开启,生产关掉——毕竟多数前端框架(React/Vue)根本不消费这些链接,纯属冗余序列化开销。
真正容易被忽略的是:total 字段要不要缓存?高频分页接口如果每次查 COUNT(*),DB 压力会直线上升。可以考虑用 Redis 缓存总数量(配合写操作更新),或者接受“近似总数”(比如用 EXPLAIN SELECT COUNT(*) 估算)。这个权衡点不在 Fiber,而在你的数据一致性要求有多高。


















