StreamBox/docs/06-gin-请求绑定与响应.md

4.1 KiB
Raw Blame History

第 6 章 Gin 请求绑定与响应

学完本章你将:优雅地绑定与校验请求参数,统一响应格式,处理错误。

6.1 参数绑定

Gin 提供 ShouldBind 系列,自动根据 Content-Type 选择绑定器:

// JSON: POST {"name":"alice","age":18}
type CreateUserReq struct {
    Name string `json:"name" binding:"required"`
    Age  int    `json:"age" binding:"gte=0,lte=120"`
}
var req CreateUserReq
if err := c.ShouldBindJSON(&req); err != nil {
    response.BadRequest(c, err.Error())
    return
}

// Query: GET /users?page=1&size=10
type ListReq struct {
    Page int `form:"page" binding:"gte=1"`
    Size int `form:"size" binding:"gte=1,lte=100"`
}
var q ListReq
c.ShouldBindQuery(&q)

// Path + Query + Form 混合:ShouldBind 自动识别
c.ShouldBind(&req)

// Header
token := c.GetHeader("Authorization")
// 默认值
page := c.DefaultQuery("page", "1")

校验标签基于 go-playground/validator,本项目已间接依赖 validator/v10。

6.2 常用校验标签

标签 说明
required 必填
gte=0 lte=120 数值范围
oneof=debug release 枚举
email url 格式
min=3 max=20 字符串长度
len=6 定长
type LoginReq struct {
    Email    string `json:"email" binding:"required,email"`
    Password string `json:"password" binding:"required,min=6"`
}

自定义校验:

if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
    v.RegisterValidation("port", func(fl validator.FieldLevel) bool {
        p := fl.Field().Int()
        return p > 0 && p < 65535
    })
}

6.3 统一响应

本项目 pkg/response/response.go 已封装:

type Result struct {
    Code int         `json:"code"`
    Msg  string      `json:"msg"`
    Data interface{} `json:"data,omitempty"`
}

func Success(c *gin.Context, data interface{}) {
    c.JSON(200, Result{Code: 0, Msg: "success", Data: data})
}
func BadRequest(c *gin.Context, msg string) {
    c.JSON(400, Result{Code: 400, Msg: msg})
}

使用:

// handler 中
func (h *PingHandler) Ping(c *gin.Context) {
    msg := h.svc.Ping()
    response.Success(c, gin.H{"message": msg})
}
// => {"code":0,"msg":"success","data":{"message":"pong"}}

func CreateUser(c *gin.Context) {
    var req CreateUserReq
    if err := c.ShouldBindJSON(&req); err != nil {
        response.BadRequest(c, err.Error())
        return
    }
    response.Success(c, gin.H{"id": 1})
}

约定:Code 0 为成功,非 0 为业务错误码;HTTP 状态码反映传输层语义(400 参数错误、401 未认证、500 服务错误)。

6.4 错误处理策略

  • 参数错误:400 BadRequest,返回校验信息
  • 业务错误:按需定义错误码,如 10001 用户不存在
  • 系统错误:500 InternalError,日志记录详情但不暴露给客户端
func Fail(c *gin.Context, httpCode, code int, msg string) {
    c.JSON(httpCode, Result{Code: code, Msg: msg})
}

建议结合 errors.Is/As 判断错误类型,在 service 层返回 error,handler 层映射为响应。

6.5 文件上传与下载

// 上传
r.POST("/upload", func(c *gin.Context) {
    file, _ := c.FormFile("file")
    c.SaveUploadedFile(file, "./uploads/"+file.Filename)
    response.Success(c, gin.H{"filename": file.Filename})
})

// 下载/返回文件
r.GET("/download/:name", func(c *gin.Context) {
    c.File("./uploads/" + c.Param("name"))
})

6.6 完整示例

type CreateBookReq struct {
    Title  string `json:"title" binding:"required"`
    Author string `json:"author" binding:"required"`
    Price  int    `json:"price" binding:"gte=0"`
}

func CreateBook(c *gin.Context) {
    var req CreateBookReq
    if err := c.ShouldBindJSON(&req); err != nil {
        response.BadRequest(c, err.Error())
        return
    }
    // 调用 service...
    response.Success(c, gin.H{"title": req.Title})
}

6.7 动手练习

  1. 新增 POST /api/v1/users,用 ShouldBindJSON + required/email 校验。
  2. 实现 GET /api/v1/users?page=&size= 分页查询,用 ShouldBindQuery。
  3. 为 BadRequest 补充单元测试,验证错误信息是否友好。