最稳妥方式是手写Offset+Limit:page≥1、pageSize∈(0,100],显式Order排序,Count需独立DB实例查询,避免复用链式条件。

分页怎么用 offset + limit 最稳妥
GORM 没有内置的 Paginate 方法(v2 仍不带),直接拼 Offset 和 Limit 是最可控、兼容性最好的方式。别信某些封装库自动算 total 的逻辑——它默认会多执行一次 COUNT(*),在大表或复杂 JOIN 场景下极易拖慢接口。
实操建议:
-
page和pageSize必须校验:拒绝page 或 <code>pageSize ,防止 SQL 注入或全表扫描 - 计算
offset用(page - 1) * pageSize,别写成page * pageSize—— 第一页就跳过首条数据 - 如果用
Find(&results),记得先声明切片容量,避免频繁扩容:results := make([]User, 0, pageSize)
动态排序必须防 SQL 注入
用户传来的 sort 字段(如 "created_at desc")不能直接拼进 Order()。GORM v2 对非法字段名会静默忽略,但若字段名含空格、逗号或函数调用(如 "id, (select 1)"),可能触发语法错误或注入。
安全做法:
立即学习“go语言免费学习笔记(深入)”;
- 预定义白名单字段和方向:
validSorts := map[string]bool{"id": true, "name": true, "created_at": true},只允许键存在且值为true的字段 - 方向严格限制为
"asc"或"desc",忽略大小写后校验,禁止传"asc, id"这类组合 - 构造排序字符串时用
fmt.Sprintf("%s %s", field, direction),再传给Order();不要用Order("?"占位符 —— GORM 不支持参数化排序
带 WHERE 条件的分页要复用查询链
很多人把 Where、Order、Limit 写成三段独立查询,结果 count 和 data 用的条件不一致(比如漏了软删除标记)。正确姿势是构建一个基础 *gorm.DB,再分别调用 Count() 和 Find()。
示例关键点:
- 先定义
db := db.Where("status = ?", 1).Where("deleted_at IS NULL")(注意软删除字段名按你项目实际改) - 查总数:
var total int64; db.Count(&total).Error,别用db.Model(&User{}).Count()—— 它会丢掉前面的Where - 查数据:
db.Offset(offset).Limit(pageSize).Order(sortClause).Find(&users),全程复用同一个db变量
GORM v2 的 Scopes 能让分页逻辑更干净
把分页和排序抽成 Scope 函数,比到处写 Offset/Limit/Order 更易维护,也方便单元测试。但注意:scope 函数内不能提前触发查询(比如调 First()),否则链式调用中断。
典型写法:
- 定义分页 scope:
func Paginate(page, pageSize int) func(db *gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB { return db.Offset((page - 1) * pageSize).Limit(pageSize) } } - 定义排序 scope:
func OrderBy(field, direction string) func(db *gorm.DB) *gorm.DB { ... },内部做白名单校验 - 组合使用:
db.Scopes(Paginate(page, size)).Scopes(OrderBy(field, dir)).Find(&users)
复杂点在于,Count() 不能直接链在 scopes 后面——它会带上 Limit。所以 count 查询得单独走一遍基础 db,再手动套 scopes 中的非分页部分(比如只套 OrderBy 前的条件)。这点容易被忽略。


















