本文代码地址:https://gitee.com/lymgoforIT/gee-web/tree/master/day4-group
本文是 7天用Go从零实现Web框架Gee教程系列
的第四篇。实现路由分组控制(Route Group Control
),代码约50
行。
分组的意义
分组控制(Group Control
)是Web
框架应提供的基础功能之一。所谓分组,是指路由的分组。如果没有路由分组,我们需要针对每一个路由进行控制。但是真实的业务场景中,往往某一组路由需要相似的处理。例如:
- 以
/post
开头的路由匿名可访问。 - 以
/admin
开头的路由需要鉴权。 - 以
/api
开头的路由是RESTful
接口,可以对接第三方平台,需要三方平台鉴权。
大部分情况下的路由分组,是以相同的前缀来区分的。因此,我们今天实现的分组控制也是以前缀来区分,并且支持分组的嵌套。例如/post
是一个分组,/post/a
和/post/b
可以是该分组下的子分组。作用在/post
分组上的中间件(middleware
),也都会作用在子分组,子分组还可以应用自己特有的中间件。
中间件可以给框架提供无限的扩展能力,应用在分组上,可以使得分组控制的收益更为明显,而不是共享相同的路由前缀这么简单。例如/admin
的分组,可以应用鉴权中间件;/
分组应用日志中间件,/
是默认的最顶层的分组,也就意味着给所有的路由,即整个框架增加了记录日志的能力。
提供扩展能力支持中间件的内容,我们将在下一节当中介绍。
分组嵌套
一个 Group
对象需要具备哪些属性呢?首先是前缀(prefix
),比如/
,或者/api
;要支持分组嵌套,那么需要知道当前分组的父亲(parent
)是谁;当然了,按照我们一开始的分析,中间件是应用在分组上的,那还需要存储应用在该分组上的中间件(middlewares
)。还记得,我们之前调用函数(*Engine).addRoute()
来映射所有的路由规则和 Handler
。如果Group
对象需要直接映射路由规则的话,比如我们想在使用框架时,这么调用:
r := gee.New()
v1 := r.Group("/v1")
v1.GET("/", func(c *gee.Context) {
c.HTML(http.StatusOK, "<h1>Hello Gee</h1>")
})
那么Group
对象,还需要有访问Router
的能力,为了方便,我们可以在Group
中,保存一个指针,指向Engine
,整个框架的所有资源都是由Engine
统一协调的,那么就可以通过Engine
间接地访问各种接口了(用了类似适配器的委托机制,让RouterGroup对象有了Engine对象的能力
)。
所以,最后的 Group
的定义是这样的:
day4-group/gee/gee.go
type RouterGroup struct {
prefix string
middlewares []HandlerFunc // support middleware
parent *RouterGroup // support nesting
engine *Engine // all groups share a Engine instance
}
我们还可以进一步地抽象,将Engine
作为最顶层的分组,也就是说Engine
拥有RouterGroup
所有的能力(用了类似适配器的委托机制,让Engine对象有了RouterGroup对象的能力
),Engine
和RouterGroup
互相包含,因此各自具备的对方的能力。
type Engine struct {
*RouterGroup
router *router
groups []*RouterGroup // store all groups
}
那我们就可以将和路由有关的函数,都交给RouterGroup
实现了。
// New is the constructor of gee.Engine
func New() *Engine {
engine := &Engine{router: newRouter()}
engine.RouterGroup = &RouterGroup{engine: engine}
engine.groups = []*RouterGroup{engine.RouterGroup}
return engine
}
// Group is defined to create a new RouterGroup
// 注意:所有的RouterGroup共享同一个Engine实例
// 此外Group方法是RouterGroup对象的,但是Engine"继承"了RouterGroup,所以也可以使用Group方法
func (group *RouterGroup) Group(prefix string) *RouterGroup {
engine := group.engine
newGroup := &RouterGroup{
prefix: group.prefix + prefix,
parent: group,
engine: engine,
}
// 将当前分组加到Engine中,后续路由匹配需要用到当前路由会调用哪些中间件
engine.groups = append(engine.groups, newGroup)
return newGroup
}
func (group *RouterGroup) addRoute(method string, comp string, handler HandlerFunc) {
pattern := group.prefix + comp
log.Printf("Route %4s - %s", method, pattern)
group.engine.router.addRoute(method, pattern, handler)
}
// GET defines the method to add GET request
func (group *RouterGroup) GET(pattern string, handler HandlerFunc) {
group.addRoute("GET", pattern, handler)
}
// POST defines the method to add POST request
func (group *RouterGroup) POST(pattern string, handler HandlerFunc) {
group.addRoute("POST", pattern, handler)
}
可以仔细观察下addRoute
函数,调用了group.engine.router.addRoute
来实现了路由的映射。由于Engine
从某种意义上继承了RouterGroup
的所有属性和方法,因为所有RouterGroup
中的engine
都是共享的同一个实例,所以不管是使用哪个RouterGroup
对象还是Engine
自身添加路由,最终路由都是添加到了同一个Engine
对象的router
字段上,这样实现,我们既可以像原来一样使用Engine对象添加路由,也可以通过分组RouterGroup对象添加路由,底层都是使用的同一个Engine对象中的Router对象在添加路由。
使用 Demo
测试框架的Demo
就可以这样写了:
func main() {
r := gee.New()
r.GET("/index", func(c *gee.Context) {
c.HTML(http.StatusOK, "<h1>Index Page</h1>")
})
v1 := r.Group("/v1")
{
v1.GET("/", func(c *gee.Context) {
c.HTML(http.StatusOK, "<h1>Hello Gee</h1>")
})
v1.GET("/hello", func(c *gee.Context) {
// expect /hello?name=geektutu
c.String(http.StatusOK, "hello %s, you're at %s\n", c.Query("name"), c.Path)
})
}
v2 := r.Group("/v2")
{
v2.GET("/hello/:name", func(c *gee.Context) {
// expect /hello/geektutu
c.String(http.StatusOK, "hello %s, you're at %s\n", c.Param("name"), c.Path)
})
v2.POST("/login", func(c *gee.Context) {
c.JSON(http.StatusOK, gee.H{
"username": c.PostForm("username"),
"password": c.PostForm("password"),
})
})
}
r.Run(":9999")
}
通过 curl
简单测试:
$ curl "http://localhost:9999/v1/hello?name=geektutu"
hello geektutu, you're at /v1/hello
$ curl "http://localhost:9999/v2/hello/geektutu"
hello geektutu, you're at /hello/geektutu
QA
-
RouterGroup结构体中的parent属性是否可以删去,因为在使用的的过程中,好像并没有需要 获取一个group的parent的地方?
答:parent 之前设计是用来拼接 prefix 的,每个 group 只记录自己的部分,最后通过 parent 层层回溯拼接。不过后来改用 group.prefix + prefix 的方式 group 初始化时已经拼接了完整的 prefix,所以不需要 parent 了,可以删除。 -
这里的 RouterGroup 应该嵌套 Engine 而不是反过来?gee.New() 的时候初始化一个 Engine,然后返回一个持有自己的 RouterGroup 就 OK。当然差别也不是特别大,不过从面向对象的角度舒服一点。
func New() *RouterGroup {
engine := &Engine{router: newRouter()}
return &RouterGroup{
Engine: engine,
}
}
func (group *RouterGroup) Group(prefix string) *RouterGroup {
newGroup := &RouterGroup{
prefix: group.prefix + prefix,
Engine: group.Engine,
}
group.Engine.groups = append(group.Engine.groups , newGroup)
return newGroup
}
答:Go语言的嵌套在其他语言中类似于继承,子类必然是比父类有更多的成员变量和方法。RouterGroup 仅仅是负责分组路由,Engine 除了分组路由外,还有很多其他的功能。RouterGroup 继承 Engine 的 Run(),ServeHTTP 等方法是没有意义的。
- 如果只是用前缀当分组的话会不会适用场景不高?我试了下如果是这个场景
v1.Group("v1")
v1.Use(gee.Logger())
v1.GET("/run", func(c *gee.Context) {
c.Json(http.StatusOK, student{})
})
r.GET("/v1/say", func(c *gee.Context) {
c.Json(http.StatusOK, student{})
})
那么 v1/say也会执行中间件的代码
所以将group设置的时候分开一下,用key去区分会不会适用场景多点?可以明确同prefix的情况下不同后缀也可以执行不同的middleware
// 分组配置
type GroupConfig struct {
Prefix string // 前缀
Key string // 分组key
}
答:这个就看如何去设计了,前缀区分一般是比较好的方式。比如对一个博客系统来说,/auth,/user 下的走授权中间件,/post/.html,就不走授权,走统计中间件。/api/ 开头的,是对外提供的公共接口,诸如此类。是比较符合 URL 设计的习惯的。特别是 Restful API,以资源为中心的 URL 设计,通过前缀做不同业务的区分更为明显,不同前缀代表不同类型的资源。
原文地址:https://geektutu.com/post/gee-day4.html