推荐用 page + size,后端转为 offset + limit;可校验边界防越界,避免前端传过大 offset 导致全表扫描;大数据量时应改用游标分页(如 WHERE created_at < ?)。

分页参数该用 page + size 还是 offset + limit?
推荐在 API 层统一用 page 和 size,后端转换为 offset 和 limit 再传给数据库。这样对前端更友好,也便于做默认值和边界校验。
常见错误是直接把 offset 暴露给前端:用户可能传 offset=9999999 导致全表扫描或超时;而 page=1000&size=20 更容易做上限拦截(比如限制 page )。
-
page从 1 开始(不是 0),避免前端混淆 -
size应设硬性上限(如最大 100),防止恶意拉取大量数据 - 若业务需精确跳转(如“加载更多”无限滚动),可额外支持
cursor模式,但不要替代page/size
Go 后端如何安全解析并校验分页参数
别用 strconv.Atoi 直接转,要结合 net/http 的 ParseForm 和自定义校验逻辑。重点防空值、负数、超限、非数字。
func parsePagination(r *http.Request) (page, size int, err error) {
r.ParseForm()
page, err = strconv.Atoi(r.FormValue("page"))
if err != nil || page < 1 {
return 0, 0, fmt.Errorf("invalid page: must be >= 1")
}
size, err = strconv.Atoi(r.FormValue("size"))
if err != nil || size < 1 || size > 100 {
return 0, 0, fmt.Errorf("invalid size: must be between 1 and 100")
}
return page, size, nil
}注意:r.FormValue 对重复 key 只取第一个,如果需要支持多值(如 page[]=1&page[]=2),得用 r.Form["page"] 并手动处理。
立即学习“go语言免费学习笔记(深入)”;
数据库查询时 offset 性能问题怎么绕开
MySQL / PostgreSQL 中 OFFSET 越大越慢,尤其在千万级表上。当 page > 1000 或 offset > 10000 时,必须考虑优化。
- 用主键/时间戳做游标分页(
WHERE created_at ) - 加覆盖索引,确保
ORDER BY和WHERE字段都在索引中 - 对管理后台类场景,可缓存总条数和高频页的
offset映射(如预计算 page 500 → offset 9980)
不要依赖 SELECT COUNT(*) 实时算总数——高并发下它会成瓶颈。改用近似值(如 MySQL 的 EXPLAIN 行数)或异步更新的统计表。
返回分页元信息时字段命名要一致且带单位
前端最怕字段名来回变:total、totalCount、total_items 混用。建议固定为:
-
total:总记录数(int) -
page:当前页码(从 1 开始) -
size:每页条数 -
pages:总页数((total + size - 1) / size) -
has_next/has_prev:布尔值,比算page 更直观
示例响应结构(JSON):
{
"data": [...],
"pagination": {
"total": 1247,
"page": 3,
"size": 20,
"pages": 63,
"has_next": true,
"has_prev": true
}
}游标分页则不用 pages 和 page,改用 next_cursor 和 prev_cursor 字符串,且不返回 total —— 这点很容易被忽略,但必须明确告知前端。









