3-熟悉路由

思考并回答以下问题:

实验介绍

本次实验的主题是路由(Router),我们将主要介绍Gin框架路由中的一些关键概念,包括如何为API方法或控制器处理函数注册到路由中,Gin框架中如何定义动态路由,对于大型项目来说如何进行路由分组,如何重定向,等等。

知识点

  • 路由注册
  • 动态路由
  • 路由分组
  • 重定向

路由注册

在之前的实验中,我们将路由比喻成住房中的房门,每一个房门(路由URL路径)对应着一个房间(控制器处理函数)。而如果我们想让Gin应用能够正确对不同URL路径的请求进行相应合适的处理,就必须得为将每个请求分配其对应的处理函数,这就需要我们显式的进行路由注册(Router Registration)。

我们现在稍微回顾一下前一个实验中的控制器代码,如下所示(我们省略了函数内容)。

1
2
3
4
5
app.GET("/", func(c *gin.Context) { ... })
app.GET("/:id", func(c *gin.Context) { ... )
app.GET("/query", func(c *gin.Context) { ... })
app.GET("/query-binding", func(c *gin.Context) { ... })
app.POST("/body-binding", func(c *gin.Context) { ... })

上面的代码在Gin引擎实例中,定义不同的几个“入口”,进入之后会经过一系列处理最终返回给客户端一些JSON或HTML响应数据。如果你还记得我们如何测试咱们的控制器的,就会注意到我们会在URL中输入不同的相对路径,也就是/、/query、/query-binding之类的。下图描述其大致的请求-路由对应关系。

因此,我们注册路由,本质上就是将请求的URL路径与对应的处理函数映射起来。

上一个实验简单介绍了一下Gin框架中IRoutes接口,这里我们再回顾一下,其定义如下(这里只保留了路由相关方法)。

1
2
3
4
5
6
7
8
9
10
11
12
type IRoutes interface {
// 路由方法
Handle(string, string, ...HandlerFunc) IRoutes
Any(string, ...HandlerFunc) IRoutes
GET(string, ...HandlerFunc) IRoutes
POST(string, ...HandlerFunc) IRoutes
DELETE(string, ...HandlerFunc) IRoutes
PATCH(string, ...HandlerFunc) IRoutes
PUT(string, ...HandlerFunc) IRoutes
OPTIONS(string, ...HandlerFunc) IRoutes
HEAD(string, ...HandlerFunc) IRoutes
}

在Gin框架中注册路由,只需要调用IRoutes对应的方法。Gin框架开发团队很贴心,已经将大部分常见的HTTP方法(GET、POST、DELETE、PATCH、PUT、OPTIONS、HEAD)内置到了标准路由库中,可以通过类似app.GET("/path/to/route", func(c *gin.Context) {...})的方式调用。如果一个HTTP请求的URL路径和HTTP方法都匹配到该路由时,就会被分发到对应的控制器处理函数里。

这里我们简单提及其中两个特殊的方法,Handle、Any。Handle跟内置方法用法类似,主要多了第一个参数,也就是HTTP方法,它的主要目的是注册不是很常见的HTTP方法,例如CONNECT等;Any也类似,但它包含了所有的HTTP方法,也就是说,当一个请求只要URL路径能匹配到该路由,就会直接分发到对应的控制器处理函数里。

动态路由

动态路由(Dynamic Routing)在Gin框架(甚至是所有Web框架)中是非常重要的概念,因为很多大型网站(例如电商)中的URL是不固定的,而且一些重要信息(例如产品ID)都会被嵌入到URL中。处理这种重复数据结构、处理逻辑的场景,正是动态路由所擅长的。之前的回顾的其中一个路由路径,/:id正是动态路由。

Gin框架中的动态路由有两种定义方式:

  • 冒号 + 参数名,表示非空参数。例如/:id,可以匹配/1001,不能匹配/或/1001/sku1。
  • 星号 + 参数名,表示可空参数。例如/me/*action,可以匹配/me/、/me/update,如果没有其他路由匹配到/me,则会重定向至/me/。

我们在实际开发中可以灵活应用这两种动态路由。

现在我们需要稍微做一些优化,为了保证main包内尽量简洁。

现在,我们在项目目录下添加一个新文件夹routes,并在routes下添加一个新文件users.go,如下图。

然后,我们在routes/users.go下输入如下内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package routes

import (
"net/http"

"github.com/gin-gonic/gin"
)

// 注册路由
func RegisterUsers(app *gin.Engine) {
app.GET("/users/:id", getUser)
app.POST("/users/:id/*action", postUserAction)
}

// 获取用户
func getUser(c *gin.Context) {
// 用户 ID
id := c.Param("id")

// 如用户不存在,返回 404
if id != "zhangsan" {
c.AbortWithStatus(http.StatusNotFound)
return
}

// 用户
user := map[string]interface{}{
"username": "zhangsan",
"role": "normal",
}

// 返回响应
c.AbortWithStatusJSON(http.StatusOK, user)
}

// 执行用户操作
func postUserAction(c *gin.Context) {
// 用户 ID
id := c.Param("id")

// 操作
action := c.Param("action")

// 如用户不存在,返回 404
if id != "zhangsan" {
c.AbortWithStatus(http.StatusNotFound)
return
}

// 返回响应
c.AbortWithStatusJSON(http.StatusOK, map[string]interface{}{
"id": id,
"action": action,
})
}

然后,打开main.go,在启动Gin引擎实例前加入如下代码。
1
2
// 注册路由 users
routes.RegisterUsers(app)

编辑完成后,我们在终端中执行go run main.go启动应用。

成功启动后,更改URL相对路径为/users/zhangsan,结果如下。

成功获得了张三的用户信息。

那么当我们试图获取李四的信息会如何呢?同样,URL相对路径更改为/users/lisi,结果如下。

返回了404找不到用户的响应。这是符合预期的。

这里可以思考一下,如果试图获取/users、/users/会怎样。先根据自己的判断猜测结果,然后自己更改URL为/users、/users/再试试。

下面我们再试一下/users/:id/*action这个动态路由。发起一个POST请求,请求相对路径为/users/zhangsan/update,结果如下。

结果符合预期,非空参数id以及可空参数action都按预期返回。

路由分组

路由分组(Routes Grouping)在大型Web应用中非常常见。分组就是将单个路由通过一定方式(例如,按URL前缀、按功能)聚合成不同类别的路由组,从而达到降低复杂度、维护成本以及提高可读性的目的。按照前一部分介绍的用户相关路由来看,它实际上前缀都是/users,如果有非常多路由,那么代码会比较冗余。路由分组带来的另一个好处是,可以允许对不同的路由注册不同的中间件,这对Web框架的权限控制尤为重要。

在Gin框架中创建一个路由组非常简单,只需要调用app.Group(relativePath string)即可。下面我们通过一个实战练习体会一下。

为了消除冗余,我们将利用路由分组来优化之前创建的关于用户的路由。

打开routes/users.go文件,在RegisterUsers中做一小部分更改,代码如下。

1
2
3
4
5
6
// 注册路由
func RegisterUsers(app *gin.Engine) {
g := app.Group("/users/:id")
g.GET("", getUser)
g.POST("*action", postUserAction)
}

然后,在终端中重新执行go run main.go运行应用。

成功启动后,再重复之前本实验动态路由中实战练习的测试部分,将会发现结果是一致的。

实际上,我们在这里做的是对代码的重构。目前看起来这个代码没什么大的变化,而那是因为我们只注册了2个路由。思考一下,如果我们的项目很大,在用户路由中有非常多的逻辑,例如50个左右接口呢?这种情况下,采用路由分组会非常明显的提升代码可读性和冗余。之后在项目实战的章节我们会用到这个技巧。

重定向

重定向(Redirect)是Web框架中非常有用的特性。当我们遇到功能改造时,很可能会遇到一些棘手情况。例如,在升级页面时,需要保留旧的路由,而在不影响用户正常使用的前提下,要让他们尽快使用新的页面。这就是会用到重定向了。

重定向的概念其实主要是来自HTTP协议,通常情况下HTTP代码为301或302,在返回的响应Header中会加入Location: <目标 URL>,表示该请求需要重定向到另一个地址。在浏览器中,如果一个请求被重定向了,则会看到URL也会变化为重定向后的目标地址。

在Gin框架中,要重定向非常简单,只需要在上下文中调用Redirect方法即可,即c.Redirect(code string, location string)

下面我们用实验来体会一下Gin框架中的重定向。

打开routes/users.go,添加一个处理函数getMe,代码如下。

1
2
3
4
// 获取当前用户
func getMe(c *gin.Context) {
c.Redirect(http.StatusFound, "/users/zhangsan")
}

将这个处理函数添加到路由,即在routes/users.go文件中的RegisterUsers方法中添加一行代码,修改后的代码如下。
1
2
3
4
5
6
7
// 注册路由
func RegisterUsers(app *gin.Engine) {
g := app.Group("/users/:id")
g.GET("", getUser)
g.POST("*action", postUserAction)
app.GET("me", getMe)
}

这样的修改表示,当请求自己的用户信息/me时,会被重定向到/users/zhangsan。

编辑完成后,在终端里执行go run main.go启动应用。

成功启动后,在浏览器URL路径中输入/me,并查看结果,会得到跟输入/users/zhangsan一模一样的结果,而且URL也变成了/users/zhangsan。

现在我们看看在HTTP协议层面发生了什么。

可以看到,这个请求的响应Status Code是302,而响应头(Response Headers)中包含一个location: /users/zhangsan值,表明目标URL是/users/zhangsan。因此,你会看到浏览器紧接着就发起了一个/users/zhangsan的GET请求。这就是重定向。

实验总结

通过本次实验,我们了解了Gin框架中关于路由的相关知识点,包括路由注册、动态路由、路由分组、重定向。这些都是Gin框架中路由的重要概念,在很多Go项目中会被经常使用。从实验过程中,我们能了解路由注册的内置方法和特殊方法,也体会了动态路由的两种定义形式,以及路由分组和重定向的实现过程。此外,我们还通过创建新文件routes/users.go,将一部分路由逻辑拆分了出来,这也是代码优化的技巧。接下来,我们将介绍Gin框架中非常重要的功能,中间件。

0%