在我们开发定义路由的时候,可能会遇到很多部分重复的路由:
/admin/users
/admin/manager
/admin/photo
以上等等,这些路由最前面的部分/admin/
是相同的,如果我们一个个写也没问题,但是不免会觉得琐碎、重复,无用劳动,那么有没有一种更好的办法来解决呢?Gin
为我们提供的解决方案就是分组路由(了解过前缀树这种数据结构的同学可能会更加容易理解一些)
分组路由
类似以上示例,就是分好组的路由,分组的原因有很多
(1)比如基于模块化,把同样模块的放在一起
(2)比如基于版本,把相同版本的API放一起,便于使用
在有的框架中,分组路由也被称之为命名空间。
假如我们现在要升级新版本APi,但是旧的版本我们又要保留以兼容老用户。那么我们使用Gin
就可以这么做
func main() {
r := gin.Default()
//V1版本的API
v1Group := r.Group("/v1")
v1Group.GET("/users", func(c *gin.Context) {
c.String(200, "/v1/users")
})
v1Group.GET("/products", func(c *gin.Context) {
c.String(200, "/v1/products")
})
//V2版本的API
v2Group := r.Group("/v2")
v2Group.GET("/users", func(c *gin.Context) {
c.String(200, "/v2/users")
})
v2Group.GET("/products", func(c *gin.Context) {
c.String(200, "/v2/products")
})
r.Run(":8080")
}
只需要通过Group
方法就可以生成一个分组,然后用这个分组来注册不同路由,用法和我们直接使用r
变量一样,非常简单。这里为了便于阅读,一般都是把不同分组的,用{}
括起来。
v1Group := r.Group("/v1")
{
v1Group.GET("/users", func(c *gin.Context) {
c.String(200, "/v1/users")
})
v1Group.GET("/products", func(c *gin.Context) {
c.String(200, "/v1/products")
})
}
v2Group := r.Group("/v2")
{
v2Group.GET("/users", func(c *gin.Context) {
c.String(200, "/v2/users")
})
v2Group.GET("/products", func(c *gin.Context) {
c.String(200, "/v2/products")
})
}
路由中间件
通过Group
方法的定义,我们可以看到,它是可以接收两个参数的:
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup
第一个就是我们注册的分组路由(命名空间);第二个是一个...HandlerFunc
,可以把它理解为这个分组路由的中间件,所以这个分组路由下的子路由在执行的时候,都会调用它。
这样就给我们带来很多的便利,比如请求的统一处理,比如/admin
分组路由下的授权校验处理。比如刚刚上面的例子:
v1Group := r.Group("/v1", func(c *gin.Context) {
fmt.Println("/v1中间件")
})
这样不管你是访问/v1/users
,还是访问/v1/products
,控制台都会打印出/v1中间件
。
分组路由嵌套
我们不光可以定义一个分组路由,还可以在这个分组路由中再添加一个分组路由,达到分组路由嵌套的目的,这种业务场景也不少,比如:
/v1/admin/users
/v1/admin/manager
/v1/admin/photo
V1版本下的admin
模块,我们使用Gin
可以这么实现。
v1AdminGroup := v1Group.Group("/admin")
{
v1AdminGroup.GET("/users", func(c *gin.Context) {
c.String(200, "/v1/admin/users")
})
v1AdminGroup.GET("/manager", func(c *gin.Context) {
c.String(200, "/v1/admin/manager")
})
v1AdminGroup.GET("/photo", func(c *gin.Context) {
c.String(200, "/v1/admin/photo")
})
}
如上代码,再调用一次Group
生成一个分组路由即可,就是这么简单,通过这种方式你还可以继续嵌套。
原理解析
那么以前这种分组路由这么方便,实现会不会很复杂呢?我们来看看源代码,分析一下它的实现方式。
在分析之前,我们先来看看我们最开始用的GET
方法签名。
// GET is a shortcut for router.Handle("GET", path, handle).
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodGet, relativePath, handlers)
}
注意第一个参数relativePath
,这是一个相对路径,也就是我们传给Gin
的是一个相对路径,那么是相对谁的呢?
现在calculateAbsolutePath
方法的源代码我们暂时不看,回过头来看Group
这个生成分组路由的方法。
// Group creates a new router group. You should add all the routes that have common middlewares or the same path prefix.
// For example, all the routes that use a common middleware for authorization could be grouped.
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
return &RouterGroup{
Handlers: group.combineHandlers(handlers),
basePath: group.calculateAbsolutePath(relativePath),
engine: group.engine,
}
}
这里要注意的是,我们通过gin.Default()
生成的gin.Engine
其实包含一个RouterGroup
(嵌套组合),所以它可以用RouterGroup
的方法。
我们先看 gin.Default()
// Default returns an Engine instance with the Logger and Recovery middleware already attached.
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery())
return engine
}
可以明显看出 engine
在 engine := New()
创建然后返回,我们继续进入New()
内部查看:
// New returns a new blank Engine instance without any middleware attached.
// By default, the configuration is:
// - RedirectTrailingSlash: true
// - RedirectFixedPath: false
// - HandleMethodNotAllowed: false
// - ForwardedByClientIP: true
// - UseRawPath: false
// - UnescapePathValues: true
func New() *Engine {
debugPrintWARNINGNew()
engine := &Engine{
RouterGroup: RouterGroup{
Handlers: nil,
basePath: "/",
root: true,
},
FuncMap: template.FuncMap{},
RedirectTrailingSlash: true,
RedirectFixedPath: false,
HandleMethodNotAllowed: false,
ForwardedByClientIP: true,
RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
TrustedPlatform: defaultPlatform,
UseRawPath: false,
RemoveExtraSlash: false,
UnescapePathValues: true,
MaxMultipartMemory: defaultMultipartMemory,
trees: make(methodTrees, 0, 9),
delims: render.Delims{Left: "{{", Right: "}}"},
secureJSONPrefix: "while(1);",
trustedProxies: []string{"0.0.0.0/0", "::/0"},
trustedCIDRs: defaultTrustedCIDRs,
}
engine.RouterGroup.engine = engine
engine.pool.New = func() any {
return engine.allocateContext()
}
return engine
}
engine := &Engine{
RouterGroup: RouterGroup{
Handlers: nil,
basePath: "/",
root: true,
},
可以看到,这里默认嵌套了 RouterGroup
。
Group
方法又生成了一个*RouterGroup
,这里最重要的就是basePath
,它的值是group.calculateAbsolutePath(relativePath)
,和我们刚刚暂停的分析的方法一样,既然这样,就来看看这个方法吧。
func (group *RouterGroup) calculateAbsolutePath(relativePath string) string {
return joinPaths(group.basePath, relativePath)
}
就是一个基于当前RouterGroup
的basePath
的路径拼接,所以我们通过Group
方法改变新生成RouterGroup
中的basePath
,就达到了路由分组的目的。
而我们通过gin.Default()
生成的最初的gin.Engine
,对应的basePath
是/
,根节点。
这是一种非常棒的代码实现方式,简单的代码,是强大的功能。
小结
分组路由的功能非常强大, 可以帮助我们进行版本的升级,模块的切分,而且它的代码实现又非常简单,这就是优秀的代码。通过分析,我们可以学到很多,也能提升很多,让自己的能力不知不觉的进步。
下面是完整的测试代码,看不懂文章的小伙伴可以一步一步debug。
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
//V1版本的API
v1Group := r.Group("/v1", func(c *gin.Context) {
fmt.Println("/v1中间件") // Goland 控制台打印
c.String(300, "/v1中间件 \n")
})
v1Group.GET("/users", func(c *gin.Context) {
c.String(200, "/v1/users")
})
v1Group.GET("/products", func(c *gin.Context) {
c.String(200, "/v1/products")
})
//V2版本的API
v2Group := r.Group("/v2")
v2Group.GET("/users", func(c *gin.Context) {
c.String(200, "/v2/users")
})
v2Group.GET("/products", func(c *gin.Context) {
c.String(200, "/v2/products")
})
r.Run(":8080")
}