Golang实现基础分页查询逻辑

分页参数校验必须做,否则OFFSET可能为负或溢出;page至少为1、size限1–100;用参数化查询防注入;总数与分页数据查两次需考虑事务一致性;Pagination结构体应通过构造函数校验参数。

分页参数校验必须做,否则 OFFSET 可能为负或溢出

Go 里常见错误是直接用 pagesizeOFFSET,却不检查边界。比如 page=0size=-10 会传给数据库导致 SQL 报错或越权读取。

建议统一用非负整数约束:

  • page 至少为 1(前端传 0 时自动转成 1
  • size 限制在 1–100 区间(防恶意拉全表)
  • 计算 offset := (page - 1) * size,确保不会溢出 int 范围(尤其 page 极大时)

database/sql 拼接 LIMITOFFSET 要用问号占位符

别用字符串拼接构造 LIMIT ?, ?,否则容易被注入或类型不匹配。MySQL/PostgreSQL 都支持参数化 LIMITOFFSET,但注意驱动差异:

  • MySQL 驱动(github.com/go-sql-driver/mysql)支持 Query 中用 ? 绑定 int 类型的 LIMIT/OFFSET
  • PostgreSQL 驱动(github.com/lib/pq)只支持 $1, $2 占位符,且 LIMIT 参数必须是 int64
  • SQLite 驱动(github.com/mattn/go-sqlite3)也支持 ?,但要求参数为 int64
rows, err := db.Query("SELECT id, name FROM users ORDER BY id LIMIT ? OFFSET ?", size, offset)
if err != nil {
    return nil, err
}

总数查询和分页数据查两次是常规做法,但要注意事务一致性

很多场景需要返回总条数(total)和当前页数据。最稳妥是执行两条 SQL:SELECT COUNT(*) + 主查询。但若业务对实时性敏感(如高并发写入),两次查询之间可能有数据变动。

应对方式:

  • 读多写少场景:忽略微小不一致,直接查两次
  • 强一致性要求:用子查询或 CTE 包裹(如 PostgreSQL 的 WITH t AS (...) SELECT *, COUNT(*) OVER() FROM t LIMIT ...),但性能略低
  • 缓存总数:对不频繁变更的表,用 Redis 缓存 COUNT 结果,配合写操作更新

封装分页结构体时,别把 PageSize 暴露成可修改字段

定义类似 Pagination 结构体时,字段应设为只读或通过构造函数控制:

type Pagination struct {
    Page  int `json:"page"`
    Size  int `json:"size"`
    Total int `json:"total"`
    Data  interface{} `json:"data"`
}

// ❌ 错误:外部可随意改 Page/Size,破坏校验逻辑 p := Pagination{Page: 0, Size: -5}

// ✅ 正确:用 NewPagination 强制校验 func NewPagination(page, size int) (*Pagination, error) { if page < 1 { page = 1 } if size < 1 || size > 100 { size = 20 } return &Pagination{Page: page, Size: size}, nil }

真正难处理的是带条件的动态分页(比如多个 WHERE 字段 + 排序字段可变),那得靠构建器模式或 SQL 模板,不是加个结构体就能解决的。