本篇是Mygin第五篇
分组控制(Group Control)是 Web 框架应该提供的基础功能之一,对同一模块功能的开发,应该有相同的前缀。或者对一部分第三方接口,统一需要加解密等功能。分组后很方便。例如:
大多数分组都是统一前缀,正确的分组可以实现子分组,无限极往下分组,当然实际情况下也不会有太多层分组。每个分组有不同的中间件(middleware),分组与子分组就像洋葱一样,一层一层往内。要想往内,需要拨开最外层,也就是要执行外层的中间件才能往内。对于分组,也有后续扩展的好处,比如/task分组,现在要统一加*问日志记录,有了分组后就可以在该分组上添加一个中间件就可以了。
在实现分组嵌套之前,先看看gin中的分组嵌套是怎么实现的。gin中的分组中含有Engin的指针,其实很好理解,因为分组要有访问Router的能力。Engin中继承了RouterGroup,实例化Engin后就可以使用Group的功能。有了Engin的指针,整个框架的所有资源都是由Engine去统一调度的,因此通过Engine可以间接地拥有整个框架的功能。你中有我我中有你。
package mygin import ( "net/http" "path" "regexp" ) // IRoutes 定义了路由组的接口 type IRoutes interface { BasePath() string 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 Match([]string, string, ...HandlerFunc) IRoutes } // anyMethods 包含所有 HTTP 方法的字符串表示 var anyMethods = []string{ http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodHead, http.MethodOptions, http.MethodDelete, http.MethodConnect, http.MethodTrace, } // RouterGroup 定义了路由组的结构体 type RouterGroup struct { Handlers HandlersChain // 路由组的中间件处理函数链 basePath string // 路由组的基础路径 engine *Engine // 路由组所属的引擎 root bool // 是否是根路由组 } // Group 创建一个新的路由组 func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup { return &RouterGroup{ Handlers: append(group.Handlers, handlers...), basePath: path.Join(group.basePath, relativePath), engine: group.engine, } } // BasePath 返回路由组的基础路径 func (group *RouterGroup) BasePath() string { return group.basePath } // handle 处理路由,将路由信息添加到引擎中 func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes { absolutePath := path.Join(group.basePath, relativePath) //处理函数,Group组定义的函数先执行,自定义的函数后执行 handlers = append(group.Handlers, handlers...) group.engine.addRoute(httpMethod, absolutePath, handlers) if group.root { return group.engine } return group } // Handle 校验 HTTP 方法的有效性,并处理路由 func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes { // 检查 HTTP 方法的有效性 if match := regexp.MustCompile("^[A-Z]+$").MatchString(httpMethod); !match { panic("http method " + httpMethod + " is not valid") } // 处理路由 return group.handle(httpMethod, relativePath, handlers) } // GET 注册 GET 方法的路由 func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes { return group.handle(http.MethodGet, relativePath, handlers) } // POST 注册 POST 方法的路由 func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes { return group.handle(http.MethodPost, relativePath, handlers) } // DELETE 注册 DELETE 方法的路由 func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) IRoutes { return group.handle(http.MethodDelete, relativePath, handlers) } // PATCH 注册 PATCH 方法的路由 func (group *RouterGroup) PATCH(relativePath string, handlers ...HandlerFunc) IRoutes { return group.handle(http.MethodPatch, relativePath, handlers) } // PUT 注册 PUT 方法的路由 func (group *RouterGroup) PUT(relativePath string, handlers ...HandlerFunc) IRoutes { return group.handle(http.MethodPut, relativePath, handlers) } // OPTIONS 注册 OPTIONS 方法的路由 func (group *RouterGroup) OPTIONS(relativePath string, handlers ...HandlerFunc) IRoutes { return group.handle(http.MethodOptions, relativePath, handlers) } // HEAD 注册 HEAD 方法的路由 func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) IRoutes { return group.handle(http.MethodHead, relativePath, handlers) } // Match 注册多个方法的路由 func (group *RouterGroup) Match(methods []string, relativePath string, handlers ...HandlerFunc) IRoutes { for _, method := range methods { group.handle(method, relativePath, handlers) } if group.root { return group.engine } return group } // Any 注册所有方法的路由 func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) IRoutes { for _, method := range anyMethods { group.handle(method, relativePath, handlers) } if group.root { return group.engine } return group }
可以看到group.engine.addRoute方法,实际调用的仍然是mygin/router.go中的addRoute方法。当group.root为true,也就是为根时,相当于没有调用Group方法,返回group.engine,也就是相当于直接调用的engine,只有当调用Group方法后才会返回group。
接下来看engine的修改
package mygin import ( "net/http" ) // HandlerFunc 定义处理函数类型 type HandlerFunc func(*Context) // HandlersChain 定义处理函数链类型 type HandlersChain []HandlerFunc // Engine 定义引擎结构,包含路由器 type Engine struct { Router RouterGroup } // ServeHTTP 实现http.Handler接口的方法,用于处理HTTP请求 func (e *Engine) ServeHTTP(w http.ResponseWriter, r *http.Request) { // 获取对应HTTP方法的路由树的根节点 root := e.trees.get(r.Method) // 解析请求路径 parts := root.parseFullPath(r.URL.Path) // 查找符合条件的节点 searchNode := make([]*node, 0) root.search(parts, &searchNode) // 没有匹配到路由 if len(searchNode) == 0 { w.Write([]byte("404 Not found!\n")) return } // 参数赋值 params := make([]Param, 0) searchPath := root.parseFullPath(searchNode[0].fullPath) for i, sp := range searchPath { if sp[0] == ':' { params = append(params, Param{ Key: sp[1:], Value: parts[i], }) } } // 获取处理函数链 handlers := searchNode[0].handlers if handlers == nil { w.Write([]byte("404 Not found!\n")) return } // 执行处理函数链 for _, handler := range handlers { handler(&Context{ Request: r, Writer: w, Params: params, }) } } // Default 返回一个默认的引擎实例 func Default() *Engine { engine := &Engine{ Router: Router{ trees: make(methodTrees, 0, 9), }, RouterGroup: RouterGroup{ Handlers: nil, basePath: "/", root: true, }, } // Group 保存 engine 的指针 engine.RouterGroup.engine = engine return engine } // Run 启动HTTP服务器的方法 func (e *Engine) Run(addr string) error { return http.ListenAndServe(addr, e) }
最后来测试一下
package main import ( "gophp/mygin" "path" ) func main() { // 创建一个默认的 mygin 实例 r := mygin.Default() group := r.Group("/api", func(context *mygin.Context) { context.String("api Group ....\n") }) group.GET("/hello/:name", func(context *mygin.Context) { name := context.Params.ByName("name") context.String(path.Join("hello ", name, "!")) }) group.GET("/hello2/:name", func(context *mygin.Context) { name := context.Params.ByName("name") context.String(path.Join("hello2 ", name, "!\n")) }) // 启动服务器并监听端口 r.Run(":8088") }
curl -i http://localhost:8088/api/hello/scott HTTP/1.1 200 OK Content-Type: text/plain; charset=utf-8 Date: Wed, 24 Jan 2024 05:16:45 GMT Content-Length: 29 api Group .... hello /scott/! ~ curl -i http://localhost:8088/api/hello2/scott HTTP/1.1 200 OK Content-Type: text/plain; charset=utf-8 Date: Wed, 24 Jan 2024 05:16:54 GMT Content-Length: 31 api Group .... hello2 /scott/!
可以看到两个路由都安装预定的返回了,即先返回Group定义的,再返回路由定义的。