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...

相关推荐
梦想很大很大9 小时前
使用 Go + Gin + Fx 构建工程化后端服务模板(gin-app 实践)
前端·后端·go
lekami_兰14 小时前
MySQL 长事务:藏在业务里的性能 “隐形杀手”
数据库·mysql·go·长事务
却尘17 小时前
一篇小白也能看懂的 Go 字符串拼接 & Builder & cap 全家桶
后端·go
ん贤18 小时前
一次批量删除引发的死锁,最终我选择不加锁
数据库·安全·go·死锁
mtngt111 天前
AI DDD重构实践
go
Grassto3 天前
12 go.sum 是如何保证依赖安全的?校验机制源码解析
安全·golang·go·哈希算法·go module
Grassto4 天前
11 Go Module 缓存机制详解
开发语言·缓存·golang·go·go module
程序设计实验室5 天前
2025年的最后一天,分享我使用go语言开发的电子书转换工具网站
go
我的golang之路果然有问题5 天前
使用 Hugo + GitHub Pages + PaperMod 主题 + Obsidian 搭建开发博客
golang·go·github·博客·个人开发·个人博客·hugo
啊汉7 天前
古文观芷App搜索方案深度解析:打造极致性能的古文搜索引擎
go·软件随想