Gin 框架学习实录 · 第5篇:用户模块增删改查 + 分页查询接口

前言

我们之前已经实现了简单的用户注册逻辑,这篇文章我们就把其他的增删改查接口都给加一下。


1. 查询用户信息 POST /info)

我们来实现一个获取用户详情的接口:根据 ID 查询用户信息,并返回用户数据。

请求结构 (request/user_request.go)

我们在 request/user_request.go 中定义参数结构体:

go 复制代码
type GetUserInfoRequest struct {
	ID uint `json:"id" binding:"required,gte=1"` // 必填,且必须大于等于 1
}

这里使用了 binding:"required,gte=1" 来做基础的参数校验,防止前端传非法 ID(如 0、负数)。

Service 层实现 (service/user_service.go)

service/user_service.go 中封装数据库查询逻辑:

go 复制代码
func GetUserByID(id uint) (*model.User, error) {
	var user model.User
	if err := config.DB.Where("id=?", id).First(&user).Error; err != nil {
		return nil, err
	}
	return &user, nil
}

控制器逻辑 (controller/user.go)

controller/user.go 中调用 service 层,处理参数和响应:

go 复制代码
func GetUserInfo(c *gin.Context) {
	var req request.GetUserInfoRequest

	if err := c.ShouldBindJSON(&req); err != nil {
		utils.Fail(c, "参数错误")
		return
	}

	user, err := service.GetUserById(req.ID)
	if err != nil {
		utils.Fail(c, "用户不存在")
		return
	}

	utils.Success(c, user)
}

这里我们使用了统一的 utils.Success / utils.Fail 方法来返回结果,接口风格统一、可维护性强。

路由注册

router/router.go 中添加路由:

go 复制代码
r.POST("/info", controller.GetUserInfo)

请求示例

http 复制代码
POST /info
Content-Type: application/json

{
  "id": 1
}

响应示例(成功)

json 复制代码
{
    "code": 0,
    "data": {
        "ID": 1,
        "Name": "Alice",
        "Age": 20
    },
    "msg": "success"
}

至此,我们已经完成了第一个查询类接口的封装,后续的列表、更新、删除接口也将遵循类似结构。

2. 更新用户信息(POST /save)

我们希望接口既支持更新,又不强制所有字段都必传,比如:

  • 只更新 name,不传 age(不改年龄)
  • 只更新 age,不传 name
  • 同时更新 name + age

请求结构体(request/user_request.go)

我们将 Age 字段改为 *int 类型,表示可以不传:

go 复制代码
type UpdateUserRequest struct {
	ID   uint   `json:"id" binding:"required,gte=1"`            // 必填
	Name string `json:"name"`                                   // 可选
	Age  *int   `json:"age" binding:"omitempty,gte=18,lte=120"` // 可选 + 范围校验
}

说明:

  • *int 表示该字段可以为 nil(未传)
  • omitempty 让未传字段不参与校验
  • 只有传了 age,才会触发 gtelte 等规则

Service 逻辑(service/user_service.go)

go 复制代码
func UpdateUser(req request.UpdateUserRequest) error {
	var user model.User
	if err := config.DB.First(&user, req.ID).Error; err != nil {
		return err
	}

	if req.Name != "" {
		user.Name = req.Name
	}
	if req.Age != nil {
		user.Age = *req.Age
	}

	return config.DB.Save(&user).Error
}
  • 如果字段未传,就不做修改
  • 使用 GORM 的 Save() 方法提交更新

控制器逻辑(controller/user.go)

go 复制代码
func UpdateUser(c *gin.Context) {
	var req request.UpdateUserRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		var ve validator.ValidationErrors
		if errors.As(err, &ve) {
			fieldMap := map[string]string{
				"Name": "用户名",
				"Age":  "年龄",
			}
			utils.Fail(c, utils.TranslateValidationError(ve, fieldMap))
		} else {
			utils.Fail(c, "参数格式不正确")
		}
		return
	}

	if err := service.UpdateUser(req); err != nil {
		utils.Fail(c, "用户信息更新失败:"+err.Error())
		return
	}

	utils.Success(c, nil)
}

路由注册

go 复制代码
r.POST("/save", controller.UpdateUser)

请求示例(仅更新用户名)

json 复制代码
POST /save
{
  "id": 1,
  "name": "Gin 新昵称"
}

响应示例(成功)

json 复制代码
{
    "code": 0,
    "data": null,
    "msg": "success"
}

3:删除用户(POST /delete)

我们来实现一个用户删除接口,支持根据用户 ID 删除数据库中的用户记录。


请求结构体(request/user_request.go)

go 复制代码
type DeleteUserRequest struct {
	ID uint `json:"id" binding:"required,gte=1"` // 必填,ID 必须大于等于 1
}

Service 层删除逻辑(service/user_service.go)

go 复制代码
func DeleteUser(id uint) error {
	var user model.User
	if err := config.DB.Where("id=?", id).First(&user).Error; err != nil {
		return err
	}
	return config.DB.Delete(&user).Error
}

说明:

  • 先查是否存在该用户
  • 再进行删除操作
  • 若用户不存在或删除失败,将返回错误信息

控制器逻辑(controller/user.go)

go 复制代码
func DeleteUser(c *gin.Context) {
	var req request.DeleteUserRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		utils.Fail(c, "参数错误")
		return
	}

	err := service.DeleteUser(req.ID)
	if err != nil {
		utils.Fail(c, "用户删除失败:"+err.Error())
		return
	}

	utils.Success(c, nil)
}

路由注册

go 复制代码
r.POST("/delete", controller.DeleteUser)

请求示例

json 复制代码
POST /delete
{
  "id": 1
}

响应示例

json 复制代码
{
  "code": 0,
  "msg": "success",
  "data": null
}

4:获取用户列表(POST /list)

用户管理模块中,分页获取用户列表是非常常见的需求。本节我们来实现一个带分页、关键词搜索、年龄筛选的接口。

请求参数结构体(request/user_request.go)

go 复制代码
type UserListRequest struct {
	Page     int    `json:"page" binding:"gte=1"`                         // 第几页(必须 >=1)
	PageSize int    `json:"page_size" binding:"gte=1,lte=100"`            // 每页条数(1-100)
	Keyword  string `json:"keyword"`                                      // 可选关键词(模糊搜索)
	MinAge   int    `json:"min_age"`                                      // 最小年龄(可选)
	MaxAge   int    `json:"max_age"`                                      // 最大年龄(可选)
}

说明:

  • 通过 binding 标签做基础参数校验
  • keyword 用于模糊搜索用户名
  • min_age / max_age 控制年龄筛选范围

Service 逻辑封装(service/user_service.go)

go 复制代码
func GetUserList(req request.UserListRequest) ([]model.User, int64, error) {
	var users []model.User
	var total int64

	db := config.DB.Model(&model.User{})

	// 关键词搜索
	if req.Keyword != "" {
		db = db.Where("name LIKE ?", "%"+req.Keyword+"%")
	}

	// 年龄筛选
	if req.MinAge > 0 {
		db = db.Where("age >= ?", req.MinAge)
	}
	if req.MaxAge > 0 {
		db = db.Where("age <= ?", req.MaxAge)
	}

	// 查询总数
	if err := db.Count(&total).Error; err != nil {
		return nil, 0, err
	}

	// 分页查询
	if err := db.Offset((req.Page - 1) * req.PageSize).
		Limit(req.PageSize).
		Find(&users).Error; err != nil {
		return nil, 0, err
	}

	return users, total, nil
}

控制器逻辑(controller/user.go)

go 复制代码
func UserList(c *gin.Context) {
	var req request.UserListRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		utils.Fail(c, "参数错误")
		return
	}

	users, total, err := service.GetUserList(req)
	if err != nil {
		utils.Fail(c, "获取用户列表失败:"+err.Error())
		return
	}

	utils.Success(c, gin.H{
		"list":      users,
		"total":     total,
		"page":      req.Page,
		"page_size": req.PageSize,
	})
}

路由注册(router/router.go)

go 复制代码
r.POST("/list", controller.UserList)

请求示例

json 复制代码
POST /list
{
  "page": 1,
  "page_size": 10,
  "keyword": "Alice",
  "min_age": 18,
  "max_age": 30
}

响应示例

json 复制代码
{
  "code": 0,
  "msg": "success",
  "data": {
    "list": [
      {
        "id": 1,
        "name": "Alice",
        "age": 20
      },
      ...
    ],
    "total": 22,
    "page": 1,
    "page_size": 10
  }
}

进一步优化

在上一步中,我们已经完成了一个支持分页、关键词搜索、年龄筛选的用户列表接口。

但我们仔细观察一下代码,会发现还有很多可以优化的地方,例如:

1. 分页参数支持默认值(更友好)

当前的分页参数是这样定义的:

go 复制代码
Page     int `json:"page" binding:"gte=1"`
PageSize int `json:"page_size" binding:"gte=1,lte=100"`

虽然做了合法性校验,但如果前端完全不传 page 和 page_size,就会校验失败,接口直接报错。

有没有更好的方式?我们可以为分页参数设置一个默认值!

2. 关键词做 trim 处理(防止空格误匹配)

比如我们现在支持按名称模糊搜索:

go 复制代码
if req.Keyword != "" {
	db = db.Where("name LIKE ?", "%"+req.Keyword+"%")
}

但如果前端传了 " Alice "(两侧带空格),就会变成:

sql 复制代码
LIKE '%  Alice  %'

搜索结果就会不准确,甚至查不到数据。

正确做法是对关键词进行 strings.TrimSpace() 操作,去除两端空格。

那我们在service/user_service.go里面是不是可以这样写:

go 复制代码
func GetUserList(req request.UserListRequest) ([]model.User, int64, error) {
    var users []model.User
    var total int64

    db := config.DB.Model(&model.User{})

    if req.Page < 1 {
       req.Page = 1
    }
    if req.PageSize < 1 || req.PageSize > 100 {
       req.PageSize = 10
    }

    req.Keyword = strings.TrimSpace(req.Keyword)

    if req.Keyword != "" {
       db = db.Where("name LIKE ?", "%"+req.Keyword+"%")
    }

    if req.MinAge > 0 {
       db = db.Where("age >= ?", req.MinAge)
    }

    if req.MaxAge > 0 {
       db = db.Where("age <= ?", req.MaxAge)
    }

    if err := db.Count(&total).Error; err != nil {
       return nil, 0, err
    }

    if err := db.Offset((req.Page - 1) * req.PageSize).Limit(req.PageSize).Find(&users).Error; err != nil {
       return nil, 0, err
    }

    return users, total, nil
}

通用分页函数封装

上面我们加强了一下参数校验这样写了之后似乎可行 不过我们后面会有各种各样的获取列表的接口 我们总不能每个接口里面都写一次这样的分页判断吧 那我们是否可以把分页单独提取出来封装成一个通用函数呢?

1.定义分页请求结构体

我们之前的列表请求参数结构体是这样的:

c 复制代码
type UserListRequest struct {
    Page     int    `json:"page" form:"page" binding:"gte=1"`
    PageSize int    `json:"page_size" form:"page_size" binding:"gte=1,lte=100"`
    Keyword  string `json:"keyword"`
    MinAge   int    `json:"min_age"`
    MaxAge   int    `json:"max_age"`
}

那我们是不是可以把Page和PageSize提取出来单独做一个request结构体呢? 比如我们在 request/page_request.go 添加:

go 复制代码
package request

type PageRequest struct {
	Page     int `json:"page" binding:"gte=1"`
	PageSize int `json:"page_size" binding:"gte=1,lte=100"`
}

然后我们之前的UserListRequest 可以嵌套它:

go 复制代码
type UserListRequest struct {
    PageRequest
    Keyword string `json:"keyword"`
    MinAge  int    `json:"min_age"`
    MaxAge  int    `json:"max_age"`
}

2.封装通用分页方法

我们可以在utils里面新建一个paginate.go:

go 复制代码
package utils

import "gorm.io/gorm"

// 泛型分页函数
func Paginate[T any](db *gorm.DB, page, pageSize int) ([]T, int64, error) {
    var list []T
    var total int64

    if page < 1 {
       page = 1
    }
    if pageSize < 1 || pageSize > 100 {
       pageSize = 10
    }

    db.Count(&total)

    err := db.Offset((page - 1) * pageSize).Limit(pageSize).Find(&list).Error
    if err != nil {
       return nil, 0, err
    }

    return list, total, nil
}

然后我们在 service 中使用它:

go 复制代码
func GetUserList(req request.UserListRequest) ([]model.User, int64, error) {

    db := config.DB.Model(&model.User{})

    req.Keyword = strings.TrimSpace(req.Keyword)

    if req.Keyword != "" {
       db = db.Where("name LIKE ?", "%"+req.Keyword+"%")
    }

    if req.MinAge > 0 {
       db = db.Where("age >= ?", req.MinAge)
    }

    if req.MaxAge > 0 {
       db = db.Where("age <= ?", req.MaxAge)
    }

    return utils.Paginate[model.User](db, req.Page, req.PageSize)
}

3.通用分页响应

我们之前在控制器里面是这样返回的:

go 复制代码
func UserList(c *gin.Context) {
    var req request.UserListRequest
    if err := c.ShouldBindJSON(&req); err != nil {
       utils.Fail(c, "参数错误")
       return
    }

    user, total, err := service.GetUserList(req)
    if err != nil {
       utils.Fail(c, "获取用户列表失败:"+err.Error())
       return
    }

    utils.Success(c, gin.H{
       "list":      user,
       "total":     total,
       "page":      req.Page,
       "page_size": req.PageSize,
    })
}

我们可以吧 返回的部分提取出来封装一个通用的分页返回结构体,我们可以放在utils/paginate.go里面:

go 复制代码
package utils

import "gorm.io/gorm"

type PageData struct {
    List     interface{} `json:"list"`
    Total    int64       `json:"total"`
    Page     int         `json:"page"`
    PageSize int         `json:"page_size"`
}

// 泛型分页函数
func Paginate[T any](db *gorm.DB, page, pageSize int) ([]T, int64, error) {
    var list []T
    var total int64

    if page < 1 {
       page = 1
    }
    if pageSize < 1 || pageSize > 100 {
       pageSize = 10
    }

    db.Count(&total)

    err := db.Offset((page - 1) * pageSize).Limit(pageSize).Find(&list).Error
    if err != nil {
       return nil, 0, err
    }

    return list, total, nil
}

这样我们的控制器返回可以改成这样:

go 复制代码
func UserList(c *gin.Context) {
    var req request.UserListRequest
    if err := c.ShouldBindJSON(&req); err != nil {
       utils.Fail(c, "参数错误")
       return
    }

    users, total, err := service.GetUserList(req)
    if err != nil {
       utils.Fail(c, "获取用户列表失败:"+err.Error())
       return
    }

    utils.Success(c, utils.PageData{
       List:     users,
       Total:    total,
       Page:     req.Page,
       PageSize: req.PageSize,
    })
}

至此,我们已经完成了分页参数、分页查询、分页响应的完整封装。

未来无论是用户列表、文章列表还是订单分页,只要:

  • 嵌套 PageRequest
  • 调用 utils.Paginate
  • 使用 PageData 统一响应

即可实现标准一致、代码最简的分页接口

项目结构继续演进(新增分页参数封装 + 通用分页函数)

go 复制代码
gin-learn-notes/
├── config/
│   └── database.go         // ✅ 数据库连接配置
│
├── controller/
│   ├── hello.go
│   ├── index.go
│   └── user.go             // ✅ 用户相关接口控制器(注册、信息、列表、更新、删除)
│
├── model/
│   └── user.go             // ✅ 用户数据模型
│
├── request/
│   ├── page_request.go     // ✅ 通用分页请求结构体 PageRequest
│   └── user_request.go     // ✅ 用户相关请求参数(注册、更新、列表等)
│
├── router/
│   └── router.go           // ✅ 注册路由 /info /save /delete /list 等
│
├── service/
│   └── user_service.go     // ✅ 用户业务逻辑(含分页查询、更新、删除、注册)
│
├── utils/
│   ├── paginate.go         // ✅ 通用分页函数 + 分页响应结构体封装
│   ├── response.go         // ✅ 统一响应格式封装 Success / Fail
│   └── validator.go        // ✅ 参数校验错误翻译工具
│
├── main.go                 // 项目入口(初始化 DB + 启动 Gin)
├── go.mod
├── .gitignore
└── README.md

统一响应模板封装(结构体 + 错误码 + 分页)

在前面的内容中,我们已经逐步完成了以下几个重要的功能模块:

  • 通用分页函数封装(包括请求结构体、分页逻辑、分页响应结构)
  • 统一响应结构(所有接口返回统一格式:code、msg、data)

接口已经非常清爽了,但我们发现:

如果想把项目的接口风格做得更专业、更完整,还缺一个重要的模块:统一的错误码定义。

为什么要封装错误码?

在实际项目中,特别是前后端分离的架构下,仅靠一个字符串 msg 是远远不够的。

我们需要更结构化、规范化的错误返回格式,方便前端根据 code 做精确判断:

json 复制代码
{
  "code": 1002,
  "msg": "用户不存在",
  "data": null
}

那我们是否可以把这三块统一提取,做成一个"响应模板"模块呢?

没错!接下来我们将统一封装:

模块 说明
response.go 响应结构体定义(统一格式) + Success/Fail 方法
code.go 错误码常量定义 + 提示信息映射
page.go 分页数据响应结构(含 list、total、page 等)

并将它们统一归入项目的 core/response/ 目录中 👇

go 复制代码
gin-learn-notes/
└── core/
    └── response/
        ├── response.go
        └── code.go
        └── page.go

接下来,我们将从封装错误码开始,进一步完善整个响应模块,构建一个真正通用、可扩展、前后端强协作的响应模板。

本篇对应代码提交记录

commit: 1664987372d56dc328959d2be80e7e59d813c652

👉 GitHub 源码地址:github.com/luokakale-k...

相关推荐
用户20066144472415 小时前
MySQL 慢查询日志开启与问题排查指南
go
喵个咪1 天前
Ent代码生成工具链
后端·go
洛卡卡了1 天前
Gin 框架学习实录 · 第9篇:实现 Redis 缓存模块封装与应用
go
洛卡卡了1 天前
Gin 框架学习实录 · 第10篇:实现用户登录功能与 JWT Token 签发及中间件验证
go
CHSnake1 天前
设计HTTP和gRPC错误码.md
后端·go
一个热爱生活的普通人1 天前
GO 扩展库: semaphore 实现原理与使用
后端·go
CyberTM1 天前
终端党的福音!开源Git命令速查工具Git Cheat Sheet TUI
git·python·go
Vespeng1 天前
Go-SJSON 组件,JSON 动态修改新方案
go
Wo3Shi4七1 天前
双向列队
数据结构·go
addaduvyhup2 天前
从 Java 的 Spring Boot MVC 转向 Go 语言开发的差异变化
java·spring boot·go·mvc