Chapter 02

路由

掌握 HTTP 方法注册、路径参数与通配、查询参数、路由分组与静态资源服务——把 URL 干净地映射到处理函数。

2.1 路由核心名词

路由(Route)
「HTTP 方法 + 路径模式」到处理函数的一条映射,如 GET /users/:id → getUser
路径参数(Path Param)
:name 声明的动态段,匹配单个路径片段,通过 c.Param("name") 读取。
通配参数(Wildcard / Catch-all)
*name 声明,匹配其后所有路径(含斜杠),只能放在路由末尾,常用于静态文件与代理。
查询参数(Query Param)
URL 问号之后的键值对,如 ?page=2&size=10,用 c.Query("page") 读取。
路由分组(RouterGroup)
共享路径前缀和中间件的一组路由,用 r.Group("/api/v1") 创建,可嵌套。
静态文件服务
把某个磁盘目录映射到 URL 前缀,让 Gin 直接返回文件内容(图片、CSS、下载)。
Any / NoRoute
Any 注册全部方法;NoRoute 处理未匹配的 404 请求。

2.2 HTTP 方法注册

r := gin.Default()

r.GET("/users", listUsers)      // 查询列表
r.POST("/users", createUser)     // 创建
r.GET("/users/:id", getUser)     // 详情
r.PUT("/users/:id", updateUser)  // 全量更新
r.PATCH("/users/:id", patchUser) // 部分更新
r.DELETE("/users/:id", delUser)  // 删除

// 一次注册多个方法
r.Any("/health", healthCheck)
r.Match([]string{"GET", "POST"}, "/form", handleForm)

2.3 路径参数 :id

r.GET("/users/:id", func(c *gin.Context) {
    id := c.Param("id")          // string
    c.JSON(200, gin.H{"user_id": id})
})

// 多个参数
r.GET("/users/:id/posts/:pid", func(c *gin.Context) {
    c.JSON(200, gin.H{
        "user": c.Param("id"),
        "post": c.Param("pid"),
    })
})
参数是 string

c.Param 永远返回 string。要转 int 请用 strconv.Atoi(id) 并处理错误;第 3 章会介绍用 ShouldBindUri 直接绑定到结构体字段并校验。

2.4 通配参数 *action

// 匹配 /files/ 之后的任意路径(含斜杠)
r.GET("/files/*filepath", func(c *gin.Context) {
    // 请求 /files/img/a/b.png -> "/img/a/b.png"
    fp := c.Param("filepath")
    c.String(200, "requested: %s", fp)
})

通配段以 / 开头返回,且只能出现在路径末尾。同一前缀下 :param*wildcard 不能冲突(radix 树会在启动时 panic 报冲突)。

2.5 查询参数与默认值

r.GET("/search", func(c *gin.Context) {
    q := c.Query("q")                       // 不存在返回 ""
    page := c.DefaultQuery("page", "1")     // 带默认值
    size, ok := c.GetQuery("size")          // ok 判断是否存在

    // 数组:?tag=go&tag=web
    tags := c.QueryArray("tag")             // []string{"go","web"}

    // map:?filter[name]=alice&filter[age]=20
    filters := c.QueryMap("filter")

    c.JSON(200, gin.H{"q": q, "page": page, "size": size, "hasSize": ok, "tags": tags, "filters": filters})
})

2.6 路由分组 Group

分组让相同前缀 + 相同中间件的路由聚在一起,是组织大型 API 的关键手段:

r := gin.Default()

// v1 版本 API
v1 := r.Group("/api/v1")
{
    v1.GET("/users", listUsers)      // -> /api/v1/users
    v1.POST("/users", createUser)

    // 嵌套分组 + 局部中间件
    admin := v1.Group("/admin", AuthRequired())
    {
        admin.DELETE("/users/:id", delUser) // -> /api/v1/admin/users/:id
    }
}

// v2 独立演进
v2 := r.Group("/api/v2")
v2.GET("/users", listUsersV2)
花括号是习惯写法

分组后的 { ... } 只是 Go 的代码块,用来视觉上圈定「这些路由属于该组」,没有语法必要性,但强烈推荐——大型项目里一眼就能看清层级。

2.7 静态文件与模板

// 把 ./public 目录映射到 /static
r.Static("/static", "./public")

// 单个文件
r.StaticFile("/favicon.ico", "./resources/favicon.ico")

// 用 embed 打包进二进制(Go 1.16+)
// r.StaticFS("/assets", http.FS(embeddedFiles))

// HTML 模板渲染
r.LoadHTMLGlob("templates/*")
r.GET("/", func(c *gin.Context) {
    c.HTML(200, "index.tmpl", gin.H{"title": "古法编程"})
})

2.8 404 与方法不允许

r.NoRoute(func(c *gin.Context) {
    c.JSON(404, gin.H{"error": "resource not found"})
})

// 开启 405 支持后可捕获方法不匹配
r.HandleMethodNotAllowed = true
r.NoMethod(func(c *gin.Context) {
    c.JSON(405, gin.H{"error": "method not allowed"})
})

2.9 路由设计小结