Files
zhuangang/topics/backend/gin/01-routing-binding.md
T

4.4 KiB
Raw Blame History

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
gin
Gin
路由
参数校验
REST
basic
backend.go.basics-types
3
backend.gin.middleware-auth
backend.gin.interview
1
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 状态码。