前言
我们之前已经实现了简单的用户注册逻辑,这篇文章我们就把其他的增删改查接口都给加一下。
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,才会触发
gte
、lte
等规则
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...