165 lines
4.1 KiB
Markdown
165 lines
4.1 KiB
Markdown
# 第 6 章 Gin 请求绑定与响应
|
||
|
||
> 学完本章你将:优雅地绑定与校验请求参数,统一响应格式,处理错误。
|
||
|
||
## 6.1 参数绑定
|
||
|
||
Gin 提供 `ShouldBind` 系列,自动根据 Content-Type 选择绑定器:
|
||
|
||
```go
|
||
// 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` | 定长 |
|
||
|
||
```go
|
||
type LoginReq struct {
|
||
Email string `json:"email" binding:"required,email"`
|
||
Password string `json:"password" binding:"required,min=6"`
|
||
}
|
||
```
|
||
|
||
自定义校验:
|
||
|
||
```go
|
||
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` 已封装:
|
||
|
||
```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})
|
||
}
|
||
```
|
||
|
||
使用:
|
||
|
||
```go
|
||
// 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`,日志记录详情但不暴露给客户端
|
||
|
||
```go
|
||
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 文件上传与下载
|
||
|
||
```go
|
||
// 上传
|
||
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 完整示例
|
||
|
||
```go
|
||
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` 补充单元测试,验证错误信息是否友好。
|