文章目录
- 分组控制
- 分组嵌套
- 中间件
前情提示:
【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 *RouterGroup
origin *Origin
}
prefix
是当前分组的前缀
middleware
中间件函数,用于中间件的实现
parent
指向父路由分组,用于支持嵌套分组
origin
是引擎对象,所有的RouterGroup指向同一个引擎实例,可以让RouterGroup也调用引擎的方法
接下来在引擎中添加路由分组对象:
type Origin struct {
*RouterGroup // 用于将origin对象抽象成最顶层的RouterGroup,使得origin可以调用RouterGroup的方法
router *router
routerGroup []*RouterGroup // 路由分组切片,存放注册的路由分组实例
}
func New() *Origin {
origin := &Origin{router: newRouter()} // 创建一个引擎对象实例
origin.RouterGroup = &RouterGroup{origin: origin} // 使用引擎对象实例化RouterGroup
origin.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.origin
newGroup := &RouterGroup{
parent: group, //将group作为父路由对象
prefix: group.prefix + prefix, // 前缀为父路由对象的前缀加上当前设置的前缀
origin: origin, // 统一引擎对象
}
origin.groups = append(origin.groups, newGroup) // 将注册的路由分组存入分组切片中
return newGroup
}
至此分组嵌套已经实现,接下来在main.go中测试:
package main
import (
"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) {
part1
c.Next()
part2
}
func B(c *Context) {
part3
c.Next()
part4
}
此时c.handlers
是这样的[A, B, Handler],接下来的流程是这样的:part1 -> part3 -> Handler -> part 4 -> part2
。
在context中模仿gin框架,改造Contex结构体:
type Context struct {
Writer http.ResponseWriter
Req *http.Request
Path string
Method string
Params map[string]string
StatusCode int
// 添加index 与 handlers 属性
index int
handlers []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 = params
if n != nil {
key := ctx.Method + "-" + n.pattern
ctx.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 kilon
func Logger() HandlerFunc {
return func(c *Context) {
// Start timer
t := time.Now()
// Process request
c.Next()
// Calculate resolution time
log.Printf("[%d] %s in %v", c.StatusCode, c.Req.RequestURI, time.Since(t))
}
}
最后在main.go中测试:
package main
import (
"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可以看到控制台输出: