上一篇跑通了第一个接口,这一篇把"请求进来后怎么取数据"讲透。路由决定哪个函数处理请求,参数绑定决定怎么把请求里的数据变成 Go 里的结构体。两块吃透,日常接口就够用了。
1. 四种 HTTP 方法
Gin 给每种方法都配了同名函数,语义和 HTTP 规范一致:
r.GET("/articles", listArticles) // 查列表 r.POST("/articles", createArticle) // 新建 r.PUT("/articles/:id", updateArticle) // 整体更新 r.DELETE("/articles/:id", deleteArticle) // 删除
还有 PATCH、HEAD、OPTIONS 以及不挑方法的 ANY:
r.Any("/health", healthCheck) // GET/POST 都接 r.PATCH("/users/:id", patchUser)
2. 路径参数
路径里带冒号的段是参数,c.Param 原样取出字符串:
r.GET("/users/:id", func(c *gin.Context) { c.JSON(200, gin.H{"id": c.Param("id")}) })
:id 只匹配单段。*filepath 匹配剩余全部路径,含斜杠:
r.GET("/static/*filepath", func(c *gin.Context) { c.JSON(200, gin.H{"path": c.Param("filepath")}) }) // 请求 /static/a/b.css -> filepath = "/a/b.css"
注意路由冲突:不能同时注册 /users/:id 和 /users/me,Gin 启动会 panic。遇到这种"固定段 vs 参数段"并存,把固定段写成独立路由,或用中间件在 handler 里判断。
3. 查询参数与表单字段
问号后的键值对用 c.Query 系列取:
r.GET("/search", func(c *gin.Context) { kw := c.Query("kw") // 无则空串 page := c.DefaultQuery("page", "1") // 无则 "1" size, _ := c.GetQuery("size") // 返回 (值, 是否存在) })
表单提交(application/x-www-form-urlencoded)用 c.PostForm:
r.POST("/form", func(c *gin.Context) { name := c.DefaultPostForm("name", "匿名") c.JSON(200, gin.H{"name": name}) })
4. 用结构体绑定 JSON
请求体是 JSON 时,定义一个结构体,挂 json 标签,ShouldBindJSON 一次性填进去:
type Article struct { Title string `json:"title" binding:"required"` Content string `json:"content" binding:"required"` Status int `json:"status"` } r.POST("/articles", func(c *gin.Context) { var a Article if err := c.ShouldBindJSON(&a); err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } c.JSON(200, gin.H{"title": a.Title, "status": a.Status}) })
binding:"required" 要求字段存在且非零值。数字缺省是 0、字符串缺省是空,所以 Status 不写就是 0,不报错。
5. binding 常用校验规则
标签里可以串多个规则,逗号分隔:
type RegisterReq struct { Username string `json:"username" binding:"required,min=3,max=20"` Email string `json:"email" binding:"required,email"` Age int `json:"age" binding:"gte=0,lte=150"` Role string `json:"role" binding:"oneof=admin user guest"` }
| 规则 | 含义 |
|---|---|
required | 不能为空/零值 |
min=N / max=N | 字符串长度或数字范围 |
gte=N / lte=N | 数字大于等于/小于等于 |
email | 邮箱格式 |
oneof=a b c | 取值必须在枚举内 |
numeric | 必须为数字字符串 |
校验失败时 err.Error() 会带字段名和规则,直接透给前端足够定位。
6. 路径参数也能绑进结构体
ShouldBindUri 把路径参数填进带 uri 标签的字段:
type UriID struct { ID uint `uri:"id" binding:"required"` } r.GET("/articles/:id", func(c *gin.Context) { var u UriID if err := c.ShouldBindUri(&u); err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } c.JSON(200, gin.H{"id": u.ID}) })
查询参数同理用 ShouldBindQuery,适合翻页、筛选这类场景。
7. 一个完整的新建接口
把上面拼起来,一个带校验的文章创建接口长这样:
func createArticle(c *gin.Context) { var req Article if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } // 这里接第 7 篇的数据库写入 c.JSON(201, gin.H{ "title": req.Title, "content": req.Content, }) }
参数怎么取、怎么校验已经清楚了。下一篇讲中间件:统一的日志、跨域、耗时统计,都靠它挂到路由上。