4.4 KiB
4.4 KiB
id, title, type, domain, stack, tags, level, prerequisites, est_hours, related, week, order
| id | title | type | domain | stack | tags | level | prerequisites | est_hours | related | week | order | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| backend.gin.routing-binding | 路由、参数绑定和校验 | lesson | backend |
|
|
basic |
|
3 |
|
|
1 |
路由、参数绑定和校验
解释说明
- 路由:
r.GET("/tasks/:id", handler),:id是路径参数,*path是通配。用r.Group("/api")给一组路由加公共前缀和中间件。Gin 的路由是前缀树(radix tree),匹配速度和路由数量关系不大。 - 取参数:路径参数
c.Param("id");查询参数c.Query("page")/c.DefaultQuery("page", "1");请求体用绑定。 - 绑定和校验:定义结构体,用标签声明来源和规则,
c.ShouldBindJSON(&req)一步完成解析和校验(底层是 go-playground/validator):
| 标签 | 含义 |
|---|---|
json:"title" |
JSON 字段名 |
form:"page" |
查询参数 / 表单字段名(配合 ShouldBindQuery) |
binding:"required,max=100" |
必填、最大长度 100 |
binding:"omitempty,oneof=todo doing done" |
可选,有值时必须是其中之一 |
binding:"gte=1,lte=100" |
数字范围 |
- 响应:
c.JSON(状态码, 数据);写完响应后要return,否则后面的代码还会执行。 - RESTful 约定:资源用名词复数(
/tasks),动作用 HTTP 方法;创建返回 201,删除成功返回 204,参数错误 400,未登录 401,无权限 403,不存在 404,冲突 409。
案例
任务列表接口要支持分页和状态筛选:GET /api/tasks?page=2&size=20&status=done。查询参数绑定到结构体,校验 size 不超过 100(防止一次拉太多数据拖垮数据库),status 只允许固定几个值。
代码示例
package main
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
type ListQuery struct {
Page int `form:"page,default=1" binding:"gte=1"`
Size int `form:"size,default=20" binding:"gte=1,lte=100"`
Status string `form:"status" binding:"omitempty,oneof=todo doing done"`
}
type CreateTask struct {
Title string `json:"title" binding:"required,max=100"`
Note string `json:"note" binding:"max=1000"`
}
func main() {
r := gin.Default() // 自带 Logger 和 Recovery 中间件
api := r.Group("/api")
{
api.GET("/tasks", func(c *gin.Context) {
var q ListQuery
if err := c.ShouldBindQuery(&q); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return // 一定要 return
}
c.JSON(http.StatusOK, gin.H{"page": q.Page, "size": q.Size, "status": q.Status, "items": []string{}})
})
api.POST("/tasks", func(c *gin.Context) {
var req CreateTask
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"id": 1, "title": req.Title})
})
api.GET("/tasks/:id", func(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil || id <= 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "id 必须是正整数"})
return
}
c.JSON(http.StatusOK, gin.H{"id": id})
})
}
_ = r.Run(":8080")
}
试一试:
curl 'localhost:8080/api/tasks?size=500' # 400:size 超过 100
curl -X POST localhost:8080/api/tasks -d '{"title":""}' -H 'Content-Type: application/json' # 400:title 必填
curl -X POST localhost:8080/api/tasks -d '{"title":"学 Gin"}' -H 'Content-Type: application/json' # 201
易错点
- 写完响应不 return:后面的代码继续执行,可能再次写响应,日志出现
headers were already written。 BindJSON和ShouldBindJSON:前者校验失败会自动写 400 且之后不能改状态码,统一用ShouldBind*自己处理。- 请求体只能读一次:中间件里读过
c.Request.Body,后面再绑定会是空的;需要多次读用c.ShouldBindBodyWith。 - 校验错误直接返回给前端:原始错误信息是英文、带结构体字段名,生产环境要转换成友好的提示(见下一课的统一错误处理)。
- 路由冲突:
/tasks/:id和/tasks/new这类静态和参数路由的组合,不同版本的 Gin 处理不同,设计路由时尽量避免歧义。
面试常问
见 Gin 面试题:路由原理、绑定和校验、RESTful 状态码。