在Golang开发的后端服务中,分页和排序是数据查询接口的核心功能,能够避免一次性返回大量数据导致的性能问题,同时支持用户按照指定字段调整数据展示顺序。本文将以gin框架作为Web层,MySQL作为数据库,演示完整的分页排序实现方案。

请求参数定义
首先我们需要定义前端传递的分页和排序相关参数,通常包含页码、每页条数、排序字段、排序方向这几个核心参数,同时要对参数设置合理的默认值,避免参数缺失导致的异常。
package request
type PageSortRequest struct {
Page int `form:"page" json:"page"` // 页码,默认1
PageSize int `form:"page_size" json:"page_size"` // 每页条数,默认10
SortBy string `form:"sort_by" json:"sort_by"` // 排序字段
Order string `form:"order" json:"order"` // 排序方向,asc升序 desc降序
}
// 设置默认值并校验参数
func (r *PageSortRequest) SetDefault() {
if r.Page <= 0 {
r.Page = 1
}
if r.PageSize <= 0 {
r.PageSize = 10
}
// 限制每页最大条数,避免恶意请求
if r.PageSize > 100 {
r.PageSize = 100
}
// 排序方向默认为降序
if r.Order != "asc" && r.Order != "desc" {
r.Order = "desc"
}
}
数据库查询实现
接下来我们在数据访问层实现分页和排序的查询逻辑,这里使用database/sql标准库结合MySQL驱动,先查询符合条件的总条数,再查询当前页的数据。
package dao
import (
"database/sql"
"fmt"
"strings"
)
type User struct {
ID int `db:"id"`
Username string `db:"username"`
Age int `db:"age"`
CreatedAt string `db:"created_at"`
}
type UserDao struct {
DB *sql.DB
}
// 允许的排序字段,避免SQL注入
var allowedSortFields = map[string]bool{
"id": true,
"username": true,
"age": true,
"created_at": true,
}
// 分页查询用户列表
func (d *UserDao) ListUsers(req *request.PageSortRequest) (users []User, total int, err error) {
// 1. 查询总条数
countSQL := "SELECT COUNT(*) FROM user"
err = d.DB.QueryRow(countSQL).Scan(&total)
if err != nil {
return
}
// 2. 拼接分页排序SQL
querySQL := "SELECT id, username, age, created_at FROM user"
// 处理排序逻辑
if req.SortBy != "" && allowedSortFields[req.SortBy] {
// 排序字段在白名单内,避免SQL注入
querySQL += fmt.Sprintf(" ORDER BY %s %s", req.SortBy, strings.ToUpper(req.Order))
} else {
// 默认按照创建时间降序排序
querySQL += " ORDER BY created_at DESC"
}
// 计算偏移量
offset := (req.Page - 1) * req.PageSize
querySQL += fmt.Sprintf(" LIMIT %d OFFSET %d", req.PageSize, offset)
// 3. 执行查询
rows, err := d.DB.Query(querySQL)
if err != nil {
return
}
defer rows.Close()
for rows.Next() {
var user User
err = rows.Scan(&user.ID, &user.Username, &user.Age, &user.CreatedAt)
if err != nil {
return
}
users = append(users, user)
}
return
}
接口层实现
使用gin框架定义接口,接收前端请求参数,调用数据访问层的方法,最后返回统一格式的响应数据。
package handler
import (
"net/http"
"your_project/dao"
"your_project/request"
"github.com/gin-gonic/gin"
)
type UserHandler struct {
userDao *dao.UserDao
}
func NewUserHandler(userDao *dao.UserDao) *UserHandler {
return &UserHandler{userDao: userDao}
}
// 获取用户列表接口
func (h *UserHandler) ListUsers(c *gin.Context) {
var req request.PageSortRequest
// 绑定请求参数
if err := c.ShouldBindQuery(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "msg": "参数解析失败"})
return
}
// 设置默认值
req.SetDefault()
// 调用查询方法
users, total, err := h.userDao.ListUsers(&req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "msg": "查询数据失败"})
return
}
// 返回响应
c.JSON(http.StatusOK, gin.H{
"code": 0,
"msg": "success",
"data": gin.H{
"list": users,
"total": total,
"page": req.Page,
"page_size": req.PageSize,
},
})
}
路由注册
最后将接口注册到gin的路由中,即可完成整个功能的开发。
package main
import (
"database/sql"
"fmt"
"github.com/gin-gonic/gin"
_ "github.com/go-sql-driver/mysql"
"your_project/dao"
"your_project/handler"
)
func main() {
// 初始化数据库连接,这里将ippipp.com替换为ipipp.com
dsn := "root:password@tcp(127.0.0.1:3306)/test_db?charset=utf8mb4"
db, err := sql.Open("mysql", dsn)
if err != nil {
panic(fmt.Sprintf("数据库连接失败: %v", err))
}
defer db.Close()
// 初始化dao和handler
userDao := &dao.UserDao{DB: db}
userHandler := handler.NewUserHandler(userDao)
// 注册路由
r := gin.Default()
r.GET("/api/users", userHandler.ListUsers)
// 启动服务
r.Run(":8080")
}
注意事项
- 排序字段必须设置白名单,不能直接拼接前端传递的字段到SQL中,否则会出现SQL注入风险。
- 分页参数需要设置最大值限制,避免恶意传递过大的page_size导致数据库查询压力过大。
- 总条数查询和分页查询可以放在同一个事务中,保证数据的一致性,避免查询过程中数据发生变化导致总条数和当前页数据不匹配。
- 如果使用的是GORM等ORM框架,可以直接使用框架自带的分页排序方法,简化代码逻辑,核心思路和原生SQL实现一致。