beego中gorm分页需显式调用find/count等终结方法,避免offset/limit链式调用丢失;应封装paginate scope函数、分开查询总数与列表,并用map返回json响应。

Beego 中直接用 DB.Offset().Limit() 分页会出错
Beego 项目里集成 GORM 后,很多人照搬 Gin 或纯 GORM 的分页写法:DB.Offset((page-1)*pageSize).Limit(pageSize).Find(&results),结果查不到数据或 panic。根本原因是没处理好 GORM 的链式调用上下文——Offset 和 Limit 返回的是新实例,但 Beego 的控制器生命周期短,若没显式触发查询(如 Find、Count),中间状态会被丢弃。
更隐蔽的问题是:GORM v2(gorm.io/gorm)中 Limit 为 0 时行为不一致(MySQL 可能返回全量,SQLite 报错),而 Beego 常见的分页参数校验缺失,容易传入 pageSize=0 导致意外。
- 务必在调用
Offset/Limit后接Find、Count或Scan等终结方法 - 在 Controller 层对
page和pageSize做边界检查,例如if page 、<code>if pageSize 100 { pageSize = 20 } - 避免复用未终结的
*gorm.DB实例做多次分页,不同请求间应各自构建查询链
用 Scopes 封装分页逻辑,避免 Controller 耦合 SQL 细节
把分页封装成可复用的 scope 函数,既清晰又利于测试。比如在 models/pagination.go 中定义:
func Paginate(page, pageSize int) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
offset := (page - 1) * pageSize
return db.Offset(offset).Limit(pageSize)
}
}
在 Controller 中直接使用:
var goods []models.Goods
err := models.DB.Scopes(
Paginate(c.GetInt64("page", 1), c.GetInt64("pageSize", 20)),
).Find(&goods).Error
这样 Controller 不关心偏移计算,也不暴露 Offset/Limit 调用顺序;后续要加缓存、审计日志或改用游标分页,只需改 Paginate 函数内部即可。
- scope 函数必须返回
func(*gorm.DB) *gorm.DB类型,否则Scopes无法识别 - 不要在 scope 里调用
Find等终结方法,否则Scopes会提前执行,无法与其他条件组合 - 若需同时获取总数,不能复用同一
Scopes结果——Count需单独构造无分页的查询链
总数统计必须另起查询,Count 不能和分页共用链式调用
常见错误是试图在一个查询链上既取数据又取总数:db.Scopes(Paginate(...)).Find(&list).Count(&total)——这会导致 Count 统计的是分页后的行数(永远 ≤ pageSize),而非全表总数。
正确做法是拆成两步:先构造不含分页的原始查询,再分别应用分页和总数统计:
db := models.DB.Model(&models.Goods{})
// 总数
var total int64
db.Count(&total).Error
// 分页数据
var list []models.Goods
db.Scopes(Paginate(page, size)).Find(&list).Error
注意 Model(&T{}) 是关键:它让 GORM 知道要查哪张表,且不带 WHERE 条件,保证 Count 结果准确。如果业务有动态 WHERE(如搜索关键词),需确保两处 WHERE 完全一致。
- 别用
db.Table("goods").Count(),它绕过 GORM 模型映射,可能忽略软删除字段(DeletedAt) - 若启用了软删除,
Count默认包含已“删除”的记录,需显式加Unscoped()或Where("deleted_at IS NULL")控制范围 - 高并发场景下,总数和分页数据可能轻微不一致(因写入发生于两次查询之间),业务上接受即可,无需强一致性
Beego 分页响应结构建议统一用 map 而非嵌套 struct
Beego Controller 返回 JSON 时,很多人习惯定义类似 type PageResp struct { List []T `json:"list"` Total int64 `json:"total"` Page int `json:"page"` } 的结构体。问题在于:每个模型都要配一个,冗余;且前端常需动态字段(如额外元信息),硬编码 struct 不灵活。
更轻量的做法是直接用 map[string]interface{} 构建响应:
c.Data["json"] = map[string]interface{}{
"code": 0,
"msg": "success",
"data": map[string]interface{}{
"list": list,
"total": total,
"page": page,
"size": size,
},
}
Beego 的 c.ServeJSON() 能正确序列化这种结构,前端也无需为每种资源维护独立 DTO。
- 避免在 map 中直接塞
err或未处理的interface{}值,可能 panic;所有值应确保可 JSON 序列化 - 若需统一错误格式,建议抽离为
JSONResp工具函数,而不是在每个 Action 里重复写 map - Beego 的
c.TplName和模板渲染与此无关,分页接口应专注返回 JSON,别混用 HTML 渲染逻辑
分页本身不难,难的是在 Beego 的 MVC 分层约束下,让数据库操作不侵入 Controller、总数与列表查询语义清晰、参数校验和错误处理不遗漏——这些细节一旦松动,线上就容易出现空列表、总数错位或 500 错误。











