文章目录
- 分组控制
- 分组嵌套
- 中间件
前情提示:
【Golang学习笔记】从零开始搭建一个Web框架(一)-CSDN博客
【Golang学习笔记】从零开始搭建一个Web框架(二)-CSDN博客
分组控制
分组控制(Group Control)是 Web 框架应提供的基础功能之一。分组指路由分组,将路由分成不同的组别,然后对每个组别应用特定的策略和规则来实现管理和控制。这些策略和规则由用户通过中间件定义。
分组嵌套
通常情况下,分组路由以前缀作为区分,现在需要实现的分组控制也以前缀区分,并且支持分组的嵌套。例如/post
是一个分组,/post/a
和/post/b
可以是该分组下的子分组。作用在/post
分组上的中间件(middleware),也都会作用在子分组,子分组还可以应用自己特有的中间件。
打开kilon.go添加一个路由分组的结构体:
type RouterGroup struct {prefix string // 前缀middlewares []HandlerFunc // 中间件函数,后续中间件的实现需要用到parent *RouterGrouporigin *Origin
}
prefix
是当前分组的前缀
middleware
中间件函数,用于中间件的实现
parent
指向父路由分组,用于支持嵌套分组
origin
是引擎对象,所有的RouterGroup指向同一个引擎实例,可以让RouterGroup也调用引擎的方法
接下来在引擎中添加路由分组对象:
type Origin struct {*RouterGroup // 用于将origin对象抽象成最顶层的RouterGroup,使得origin可以调用RouterGroup的方法router *routerrouterGroup []*RouterGroup // 路由分组切片,存放注册的路由分组实例
}
func New() *Origin {origin := &Origin{router: newRouter()} // 创建一个引擎对象实例origin.RouterGroup = &RouterGroup{origin: origin} // 使用引擎对象实例化RouterGrouporigin.groups = []*RouterGroup{origin.RouterGroup} // 将origin.RouterGroup作为所有分组的父分组return origin
}
将路由都交给路由分组对象进行管理:
func (group *RouterGroup) addRoute(method string, comp string, handler HandlerFunc) {pattern := group.prefix + comp // pattern为分组前缀prefix加上当前注册的路径log.Printf("Route %4s - %s",method, pattern) group.origin.router.addRoute(method, pattern, handler)
}func (group *RouterGroup) GET(pattern string, hander HandlerFunc) {group.addRoute("GET", pattern, hander)
} // 修改func (group *RouterGroup) POST(pattern string, hander HandlerFunc) {group.addRoute("POST", pattern, hander)
} // 修改
接下来需要编写分组注册的方法:
func (group *RouterGroup) Group(prefix string) *RouterGroup {origin := group.originnewGroup := &RouterGroup{parent: group, //将group作为父路由对象prefix: group.prefix + prefix, // 前缀为父路由对象的前缀加上当前设置的前缀 origin: origin, // 统一引擎对象}origin.groups = append(origin.groups, newGroup) // 将注册的路由分组存入分组切片中return newGroup
}
至此分组嵌套已经实现,接下来在main.go中测试:
package mainimport ("fmt""kilon""net/http"
)func main() {r := kilon.New()group1 := r.Group("/hello")group1.GET("/:username", func(ctx *kilon.Context) {ctx.JSON(http.StatusOK, kilon.H{"message": fmt.Sprintf("Hello %s", ctx.Param("username")),})})group2 := r.Group("/file")group2.GET("/:filename", func(ctx *kilon.Context) {ctx.JSON(http.StatusOK, kilon.H{"file": fmt.Sprintf("zhangsan's %s", ctx.Param("filename")),})})r.Run(":8080")
}
浏览器分别访问:127.0.0.1:8080/hello/zhangsan 与 127.0.0.1:8080/file/photo.png
可以看到返回的JSON数据
中间件
在Web框架中,中间件用于处理HTTP请求和响应的过程中,对请求进行预处理、后处理或者进行一些额外的操作。中间件提供了一种灵活的方式来扩展和定制Web应用程序的功能。
这里的中间件设计参考了Gin框架的实现。在gin框架的context.go中(gin/context.go),中间件的实现主要与上下文对象中index与handlers两个属性以及Next方法有关:
// type HandlerFunc func(*Context)
// type HandlersChain []HandlerFunc
type Context struct {...handlers HandlersChain // HandlerFunc 的切片,用于按顺序执行多个中间件函数。index int8 // 表示当前需要执行哪个中间处理函数,与下面的next方法关联...
}
当调用Next方法时,c.index++,将控制权交给下一个中间件函数。(循环的好处在于,前置中间件可以不调用Next方法,减少代码重复)
func (c *Context) Next() {c.index++for c.index < int8(len(c.handlers)) {c.handlers[c.index](c) c.index++}
}
handlers 最后会放入用户路由注册的函数handler,基本的处理流程是这样的:当有一个请求到来时,服务器会创建一个 Context
对象来存储请求的相关信息,然后依次调用存储在 handlers
字段中的中间件函数(按照添加的顺序),并将当前的 Context
对象传递给这些函数。中间件函数函数调用Next方法后,会将控制权交给下一个中间件函数,直到所有中间件函数都执行完毕,最终处理请求的函数会被调用。如注册了下面两个中间件:
func A(c *Context) {part1c.Next()part2
}
func B(c *Context) {part3c.Next()part4
}
此时c.handlers
是这样的[A, B, Handler],接下来的流程是这样的:part1 -> part3 -> Handler -> part 4 -> part2
。
在context中模仿gin框架,改造Contex结构体:
type Context struct {Writer http.ResponseWriterReq *http.RequestPath stringMethod stringParams map[string]stringStatusCode int// 添加index 与 handlers 属性index inthandlers []HandlerFunc
}func newContext(w http.ResponseWriter, req *http.Request) *Context {return &Context{Writer: w,Req: req,Path: req.URL.Path,Method: req.Method,index: -1, // 初始化为-1}
}
// 定义Next方法
func (c *Context) Next() {c.index++for c.index < len(c.handlers){c.handlers[c.index](c) // 调用中间件函数c.index++}
}
在kilon中添加分组路由对象绑定中间件的方法:
func (group *RouterGroup) Use(middleware ...HandlerFunc){group.middleware = append(group.middleware, middleware...)
}
修改ServeHTTP接口的实现,将中间件赋予上下文对象:
func (origin *Origin) ServeHTTP(w http.ResponseWriter, req *http.Request) { var middlewares []HandlerFunc// 寻找所属路由分组for _, group := range origin.groups {// 将该路由分组的中间件取出if strings.HasPrefix(req.URL.Path, group.prefix) {middlewares = append(middlewares, group.middlewares...)}} ctx := newContext(w, req) // 创建上下文对象ctx.handlers = middlewares // 将中间件赋予上下文对象origin.router.handle(ctx) // 在handle中将用户的路由注册的函数放入上下文对象的handlers中
}
在router.go的handle方法中,将路由映射的函数放入上下文对象的handlers中:
func (r *router) handle(ctx *Context) {n, params := r.getRoute(ctx.Method, ctx.Path)ctx.Params = paramsif n != nil {key := ctx.Method + "-" + n.patternctx.handlers = append(ctx.handlers, r.Handlers[key]) // 将路由映射的函数放入上下文对象的handlers最后} else {ctx.handlers = append(ctx.handlers, func(c *Context) {c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)})}ctx.Next() // 中间件,启动!
}
此外,还需要定义一个方法,当请求不符合要求时,中件间可以直接跳过之后的所有处理函数,并返回错误信息:
func (c *Context) Fail(code int, err string) {c.index = len(c.handlers)c.JSON(code, H{"message": err})
}
下面实现通用的Logger
中间件,能够记录请求到响应所花费的时间。
新建文件klogger.go,当前目录结构如下:
myframe/├── kilon/│ ├── context.go│ ├── go.mod [1]│ ├── kilon.go│ ├── klogger.go│ ├── router.go│ ├── tire.go├── go.mod [2]├── main.go
向klogger.go中写入:
package kilonfunc Logger() HandlerFunc {return func(c *Context) {// Start timert := time.Now()// Process requestc.Next()// Calculate resolution timelog.Printf("[%d] %s in %v", c.StatusCode, c.Req.RequestURI, time.Since(t))}
}
最后在main.go中测试:
package mainimport ("fmt""kilon""net/http"
)func A() kilon.HandlerFunc{return func (c *kilon.Context) {fmt.Println("part1")c.Next()fmt.Println("part2")}
}func B() kilon.HandlerFunc{return func (c *kilon.Context) {fmt.Println("part3")c.Next()fmt.Println("part4")}
}func main() {r := kilon.New()group := r.Group("/hello")group.Use(kilon.Logger())group.Use(A(),B())group.GET("/:username", func(ctx *kilon.Context) {ctx.JSON(http.StatusOK, kilon.H{"message": fmt.Sprintf("Hello %s", ctx.Param("username")),})})r.Run(":8080")
}
访问127.0.0.1:8080/hello/zhangsan可以看到控制台输出: