StreamBox/docs/04-gin-路由与分组.md

3.6 KiB

第 4 章 Gin 路由与分组

学完本章你将:熟练定义路由、处理路径/查询参数、组织分组、设计 RESTful API。

4.1 路由基础

Gin 支持所有 HTTP 方法:

r.GET("/ping", handler)
r.POST("/users", handler)
r.PUT("/users/:id", handler)
r.DELETE("/users/:id", handler)
r.PATCH("/users/:id", handler)
r.OPTIONS("/*any", handler)
r.Any("/any", handler) // 匹配任意方法

本项目示例:

r.GET("/health", health.Check)        // 健康检查
v1 := r.Group("/api/v1")
v1.GET("/ping", pingHandler.Ping)     // 分组路由

4.2 路径参数

// 定义
r.GET("/users/:id", func(c *gin.Context) {
    id := c.Param("id") // "/users/42" => "42"
})

// 多参数
r.GET("/users/:id/books/:bookID", func(c *gin.Context) {
    id := c.Param("id")
    bookID := c.Param("bookID")
})

// 通配符(匹配剩余路径)
r.GET("/files/*filepath", func(c *gin.Context) {
    fp := c.Param("filepath") // "/files/a/b.txt" => "/a/b.txt"
})

4.3 查询参数与表单

// GET /search?q=gin&page=2
r.GET("/search", func(c *gin.Context) {
    q := c.Query("q")                    // "gin"
    page := c.DefaultQuery("page", "1")  // 带默认值
    arr := c.QueryArray("tag")           // ?tag=a&tag=b => [a b]
    m := c.QueryMap("filter")            // ?filter[name]=a => map[name:a]
})

// POST form
r.POST("/form", func(c *gin.Context) {
    name := c.PostForm("name")
    file, _ := c.FormFile("upload")
    c.SaveUploadedFile(file, "./"+file.Filename)
})

4.4 分组路由

分组用于版本、权限、模块划分:

v1 := r.Group("/api/v1")
{
    v1.GET("/ping", pingHandler.Ping)
    v1.GET("/users", listUsers)
    v1.POST("/users", createUser)
}

// 嵌套分组
admin := v1.Group("/admin", authMiddleware())
{
    admin.GET("/stats", statsHandler)
}

本项目 internal/router/router.go 即采用分组:r.GET("/health") 独立于 v1.Group("/api/v1"),便于探活不走鉴权。

4.5 RESTful 设计示例

以用户资源为例:

方法 路径 说明
GET /api/v1/users 列表
POST /api/v1/users 创建
GET /api/v1/users/:id 详情
PUT /api/v1/users/:id 全量更新
PATCH /api/v1/users/:id 部分更新
DELETE /api/v1/users/:id 删除
users := v1.Group("/users")
{
    users.GET("", listUsers)
    users.POST("", createUser)
    users.GET("/:id", getUser)
    users.PUT("/:id", updateUser)
    users.DELETE("/:id", deleteUser)
}

4.6 路由注册实战(对照本项目)

internal/router/router.go 完整模式:

func New(cfg *config.Config) *gin.Engine {
    gin.SetMode(cfg.Server.Mode)
    r := gin.New()
    r.Use(middleware.Recovery())
    r.Use(middleware.ZapLogger())
    r.Use(middleware.Cors(cfg.Cors))

    health := handler.NewHealthHandler()
    r.GET("/health", health.Check)

    pingRepo := repo.NewPingRepo()
    pingSvc := service.NewPingService(pingRepo)
    pingHandler := handler.NewPingHandler(pingSvc)

    v1 := r.Group("/api/v1")
    { v1.GET("/ping", pingHandler.Ping) }

    return r
}

依赖注入顺序:repo -> service -> handler -> router,各层职责清晰,新增模块时复制此模式即可。

4.7 重定向与 NoRoute

r.GET("/old", func(c *gin.Context) {
    c.Redirect(301, "/new")
})
r.NoRoute(func(c *gin.Context) {
    c.JSON(404, gin.H{"code": 404, "msg": "not found"})
})

4.8 动手练习

  1. 新增 GET /api/v1/users/:id,返回 {"id": "xxx"}。
  2. 实现 GET /search 支持 q 与 page 查询参数,page 默认 1。
  3. 将 /health 与 /api/v1/* 分为两组,给后者单独加一个日志中间件。