分页参数校验必须做,否则OFFSET可能为负或溢出;page至少为1、size限1–100;用参数化查询防注入;总数与分页数据查两次需考虑事务一致性;Pagination结构体应通过构造函数校验参数。
OFFSET 可能为负或溢出Go 里常见错误是直接用 page 和 size 算 OFFSET,却不检查边界。比如 page=0 或 size=-10 会传给数据库导致 SQL 报错或越权读取。
建议统一用非负整数约束:
page 至少为 1(前端传 0 时自动转成 1)size 限制在 1–100 区间(防恶意拉全表)offset := (page - 1) * size,确保不会溢出 int 范围(尤其 page 极大时)database/sql 拼接 LIMIT 和 OFFSET 要用问号占位符别用字符串拼接构造 LIMIT ?, ?,否则容易被注入或类型不匹配。MySQL/PostgreSQL 都支持参数化 LIMIT 和 OFFSET,但
注意驱动差异:
github.com/go-sql-driver/mysql)支持 Query 中用 ? 绑定 int 类型的 LIMIT/OFFSET
github.com/lib/pq)只支持 $1, $2 占位符,且 LIMIT 参数必须是 int64
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(*) + 主查询。但若业务对实时性敏感(如高并发写入),两次查询之间可能有数据变动。
应对方式:
WITH t AS (...) SELECT *, COUNT(*) OVER() FROM t LIMIT ...),但性能略低COUNT 结果,配合写操作更新Page 和 Size 暴露成可修改字段定义类似 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 模板,不是加个结构体就能解决的。