Dawn's Blogs

分享技术 记录成长

0%

从零实现一个Web框架 (5) 中间件

在本节,最终的代码目录结构如下:

1
2
3
4
5
6
7
8
9
dain/
|--context.go
|--dain.go
|--logger.go
|--router.go
|--trie.go
|--go.mod
main.go
go.mod

实现目标

实现中间件的添加以及 Logger 中间件(用于记录请求处理时间和响应码)。

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
package main

import (
"DawnGin/dain"
"net/http"
)

func main() {
e := dain.New()

// 使用 Logger 中间件
e.Use(dain.Logger())

e.Get("/hello/:name", func(c *dain.Context) {
name := c.Param("name")
c.String(http.StatusOK, "Hello, you are %v, URL path = %v", name, c.Path)
})

// 分组路由
v1 := e.Group("/v1")
{
v1.Get("/video/:name", func(c *dain.Context) {
videoName := c.Param("name")
c.String(http.StatusOK, "Hello, this is v1 group, video name = %v, path = %v", videoName, c.Path)
})
}

e.Run(":9000")
}

中间件

Context

dain/context

在上下文 Context 中需要保存中间件信息,以及需要保存执行到第几个中间件了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type Context struct {
// HTTP 请求 响应
Writer http.ResponseWriter
Req *http.Request
// 请求信息
Path string // 请求路径
Method string // 请求方法
Params map[string]string // 路由参数,如 /hello/:user 匹配 /hello/dawn,则 Params["user"]=dawn
// 响应信息
StatusCode int // 响应码
// 中间件
handlers []HandlerFunc // 存储中间件
index int // 执行的中间件下标
}

func NewContext(w http.ResponseWriter, r *http.Request) *Context {
return &Context{
Writer: w,
Req: r,
Path: r.URL.Path,
Method: r.Method,
index: -1,
}
}

c.Next 方法可以执行下一个中间件,实现在执行下一个中间件之后再进行一些额外的操作:

1
2
3
4
5
6
7
// Next 执行下一个中间件
func (c *Context) Next() {
c.index++
for ; c.index < len(c.handlers); c.index++ {
c.handlers[c.index](c)
}
}

将中间件应用到 Group

dain/dain.go

定义 Use 函数,用于将中间件添加到 Group 中。

1
2
3
4
// Use 为分组添加中间件
func (group *RouterGroup) Use(middlewares ...HandlerFunc) {
group.middleware = append(group.middleware, middlewares...)
}

同时,需要重写 ServeHTTP 函数,当我们接收到一个具体请求时,要判断该请求适用于哪些中间件,在这里我们简单通过 URL 的前缀来判断,将对应的中间件加入到 context 中。

1
2
3
4
5
6
7
8
9
10
// 实现 http.Handler 接口,自定义路由器
func (e *Engine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
c := NewContext(w, r)
for _, group := range e.groups {
if strings.HasPrefix(r.URL.Path, group.prefix) {
c.handlers = append(c.handlers, group.middleware...)
}
}
e.router.handle(c)
}

修改 router

dain/router.go

需要修改 router.handle 方法,将最后的请求处理逻辑 Handler 添加在中间件的最后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// handle 实现路由功能
func (r *router) handle(c *Context) {
// 在前缀树种查找路由,获取路由参数
n, params := r.getRoute(c.Method, c.Path)

if n != nil {
// 匹配路由
key := c.Method + "-" + n.pattern
c.Params = params
c.handlers = append(c.handlers, r.handlers[key])
} else {
c.handlers = append(c.handlers, func(c *Context) {
c.String(http.StatusNotFound, "404 NOT FOUND FOR PATH: %v", c.Path)
})
}

c.Next()
}

Logger 中间件

dain/logger.go

预定义一个 Logger 中间件,用于记录每一个请求的处理时间、响应码

1
2
3
4
5
6
7
8
9
10
func Logger() HandlerFunc {
return func(c *Context) {
// 开始时间
startTime := time.Now()
// 向后处理请求
c.Next()
// 处理结束,输出日志
log.Printf("[%d] %s in %v", c.StatusCode, c.Req.RequestURI, time.Since(startTime))
}
}