必须手写Limit+Offset分页,禁用Paginate封装;page和page_size需结构体绑定校验(min=1,max=100)并400返回;Order BY须加主键兜底如created_at DESC, id DESC且建联合索引;Count查询须用子查询或新Session隔离JOIN污染。

分页必须手写 Limit + Offset,别信 Paginate 封装
GORM 官方从 v1 到 v2 都没提供 Paginate 方法,所有“自动分页”都是第三方封装或自定义函数。电商商品列表对数据一致性要求高,用封装容易掩盖 Offset 计算错误、Order 缺失、总数查询污染等关键问题。直接组合 Limit 和 Offset 最可控——尤其在促销期间商品频繁上下架时,能避免因封装层默认行为导致的漏单或重复展示。
参数校验不只防 panic,更防库存/价格错乱
电商场景下,page 和 page_size 若未经强校验,可能引发连锁问题:
-
page_size = 0或负数:MySQL 可能返回全表,暴露出未上架商品或敏感字段 -
page_size > 100:一次拉取过多商品,触发 Redis 缓存击穿或下游库存服务限流 -
page = -1或非数字:Gin 的ShouldBindQuery会报错,但若 fallback 成page = 1而不记录日志,运营人员查不到“第 0 页”点击来源
正确做法是:用结构体绑定 + binding:"required,min=1,max=100" 标签,非法值直接 400 返回,并打点监控异常请求频率。
Order BY 必须带索引字段,且不能只用 created_at
商品列表排序常按 created_at DESC,但多个商品可能同秒上架,数据库不保证二级顺序。结果就是:用户翻页时某商品在第 3 页出现两次,第 4 页又消失。
安全写法是加主键兜底:
db.Order("created_at DESC, id DESC")
同时确认 (created_at, id) 是联合索引(或至少 id 单独有索引)。否则 OFFSET 越大,执行计划越可能走全表扫描——大促期间首页加载延迟直接飙升。
Count 查询必须隔离 Session,尤其带 JOIN 时
电商商品常需关联查分类、品牌、SKU 库存,写成:
db.Joins("JOIN categories ON products.category_id = categories.id").
Where("categories.status = ?", "online").
Count(&total)
这会生成 SELECT COUNT(*) FROM products JOIN categories ...,但实际要的是“在线分类下的商品总数”,不是“在线分类和商品的笛卡尔积数”。
正确姿势是用子查询或新建 Session:
- 子查询(推荐):
db.Table("(SELECT id FROM products WHERE category_id IN (SELECT id FROM categories WHERE status = 'online')) AS p").Count(&total) - 新 Session:
db.Session(&gorm.Session{NewDB: true}).Model(&Product{}).Where(...).Count(&total)
漏掉这步,后台管理页显示“共 1200 件商品”,实际只查出 800 条,运营会以为数据丢了。
游标分页在商品详情页“猜你喜欢”里更合适,但标准商品列表仍得用 Offset——因为用户要跳转任意页码,而游标无法支持。这时候唯一能压住性能的,是覆盖索引 + 严格参数截断 + 独立 Count。


















