← 返回博客
go2026-09-12 20:29:382 分钟 · 586 0

Gin 路由与参数绑定:路径、查询、JSON 一次讲清

讲清 Gin 的 GET/POST/PUT/DELETE 路由、路径参数与查询参数的取法,以及用结构体绑定 JSON 表单并做字段校验。

#gin#路由#参数绑定

上一篇跑通了第一个接口,这一篇把"请求进来后怎么取数据"讲透。路由决定哪个函数处理请求,参数绑定决定怎么把请求里的数据变成 Go 里的结构体。两块吃透,日常接口就够用了。

1. 四种 HTTP 方法

Gin 给每种方法都配了同名函数,语义和 HTTP 规范一致:

r.GET("/articles", listArticles)     // 查列表
r.POST("/articles", createArticle)   // 新建
r.PUT("/articles/:id", updateArticle) // 整体更新
r.DELETE("/articles/:id", deleteArticle) // 删除

还有 PATCHHEADOPTIONS 以及不挑方法的 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,
    })
}

参数怎么取、怎么校验已经清楚了。下一篇讲中间件:统一的日志、跨域、耗时统计,都靠它挂到路由上。

相关推荐

本文为原创文章,采用CC BY-NC-SA 4.0协议授权,转载请保留署名与原文链接。原文链接:https://www.wxbuluo.com/article/178