源代码/数据集已上传到
Github - 7days-golang
本文是 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对象需要直接映射路由规则的话,比如我们想在使用框架时,这么调用:
1 2 3 4 5 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间接地访问各种接口了。
所以,最后的 Group 的定义是这样的:
day4-group/gee/gee.go
1 2 3 4 5 6 RouterGroup struct { prefix string middlewares []HandlerFunc parent *RouterGroup engine *Engine }
我们还可以进一步地抽象,将Engine作为最顶层的分组,也就是说Engine拥有RouterGroup所有的能力。
1 2 3 4 5 Engine struct { *RouterGroup router *router groups []*RouterGroup }
那我们就可以将和路由有关的函数,都交给RouterGroup实现了。
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 func New () *Engine { engine := &Engine{router: newRouter()} engine.RouterGroup = &RouterGroup{engine: engine} engine.groups = []*RouterGroup{engine.RouterGroup} return engine } func (group *RouterGroup) Group(prefix string ) *RouterGroup { engine := group.engine newGroup := &RouterGroup{ prefix: group.prefix + prefix, parent: group, 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) } func (group *RouterGroup) GET(pattern string , handler HandlerFunc) { group.addRoute("GET" , pattern, handler) } func (group *RouterGroup) POST(pattern string , handler HandlerFunc) { group.addRoute("POST" , pattern, handler) }
可以仔细观察下addRoute函数,调用了group.engine.router.addRoute来实现了路由的映射。由于Engine从某种意义上继承了RouterGroup的所有属性和方法,因为 (*Engine).engine 是指向自己的。这样实现,我们既可以像原来一样添加路由,也可以通过分组添加路由。
使用 Demo测试框架的Demo就可以这样写了:
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 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) { 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) { 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 简单测试:
1 2 3 4 5 $ 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
last updated at 2026-02-23