155 lines
3.6 KiB
Markdown
155 lines
3.6 KiB
Markdown
# 第 4 章 Gin 路由与分组
|
|
|
|
> 学完本章你将:熟练定义路由、处理路径/查询参数、组织分组、设计 RESTful API。
|
|
|
|
## 4.1 路由基础
|
|
|
|
Gin 支持所有 HTTP 方法:
|
|
|
|
```go
|
|
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) // 匹配任意方法
|
|
```
|
|
|
|
本项目示例:
|
|
|
|
```go
|
|
r.GET("/health", health.Check) // 健康检查
|
|
v1 := r.Group("/api/v1")
|
|
v1.GET("/ping", pingHandler.Ping) // 分组路由
|
|
```
|
|
|
|
## 4.2 路径参数
|
|
|
|
```go
|
|
// 定义
|
|
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 查询参数与表单
|
|
|
|
```go
|
|
// 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 分组路由
|
|
|
|
分组用于版本、权限、模块划分:
|
|
|
|
```go
|
|
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 | 删除 |
|
|
|
|
```go
|
|
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` 完整模式:
|
|
|
|
```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
|
|
|
|
```go
|
|
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/*` 分为两组,给后者单独加一个日志中间件。
|