echo框架最巧妙的地方在于echo内部结构体的设计,核心的地方在于路由是如何通过lcp(最长前缀匹配)插入到字典树中,又是如何通过lcp找到对应的路由并且通过链式结构保重中间件按照顺序运行的设计思路,比如:
echo的中间件逻辑很有趣,他采用了一种链式的结构来保证各种级别的中间件(根组级,组级,路由级)的执行顺序,下面我将以group组下的路由注册为例,来解释中间件的如何保证顺序的,示例代码如下:
package main
import (
"net/http"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
func main() {
//声明echo实例
e := echo.New()
//注册根组中间件
e.Use(middleware.Recover())
//创建一个子组/admin
g := e.Group("/admin")
//在使用子组中间件之前注册一个路由
g.GET("/", hello)
//组级中间件
g.Use(middleware.Logger())
//在使用子组中间件之后注册一个路由
g.GET("/log", log)
//启动echo
e.Logger.Fatal(e.Start(":1323"))
}
func hello(c echo.Context) error {
return c.String(http.StatusOK, "hello world!")
}
func log(c echo.Context) error {
return c.String(http.StatusOK, "logger active")
}
1. 注册子组
可以看到注册子组的时候,并没有在echo中加入子组,子组也没有继承根组的所有中间件,仅仅是声明了group结构体
// Group creates a new router group with prefix and optional group-level middleware.
func (e *Echo) Group(prefix string, m ...MiddlewareFunc) (g *Group) {
g = &Group{prefix: prefix, echo: e}
g.Use(m...)
return
}
2.注册子组级的路由
所有的GET,POST,PATCH,PULL等方法都会调用add来加入路由中,这里的h就是我们的逻辑处理函数hello
// GET implements `Echo#GET()` for sub-routes within the Group.
func (g *Group) GET(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
return g.Add(http.MethodGet, path, h, m...)
}
3.添加路由
可以看到在添加路由的时候,他把当前路由特有的中间件,而在这个子组级别声明的所有中间件都频道了一起,并且通过前缀prefix的拼接拼出来了全路径,然后调用了echo.add加入到echo实例中的路由,而不是加入到group级别的路由
// Add implements `Echo#Add()` for sub-routes within the Group.
func (g *Group) Add(method, path string, handler HandlerFunc, middleware ...MiddlewareFunc) *Route {
// Combine into a new slice to avoid accidentally passing the same slice for
// multiple routes, which would lead to later add() calls overwriting the
// middleware from earlier calls.
m := make([]MiddlewareFunc, 0, len(g.middleware)+len(middleware))
m = append(m, g.middleware...)
m = append(m, middleware...)
return g.echo.add(g.host, method, g.prefix+path, handler, m...)
}
4.加入字典树
middware:这里的中间件有2个来源
1种是来自于直接调用e.add(),此时middleware是表示来自于路由级别的专属中间件,当这个路由被匹配到的时候触发
2中是来自于group.echo.add(),此时的middleware是来自路由中间件+组中间件 按先后顺序拼起来的,当这个路由被撇配到的时候触发
也就是说不论是嵌套了多少层的group他最后的路由都是加在根组echo中,并且会把每一层的·组中间件·+ 最后的·路由中间件· 全都拼起来
这样如果组嵌套的很深,组中间件很多,路由很多的时候,就会有大量的重复中间件保存在hanlder中
这里就能看出来他把组中间件也作为handler一起加入了路由的字典树中,并且已经拍好了顺序,可以判断这个逻辑就是导致为什么echo,use没有顺序,而group,use有顺序
//host:""
//path:"/admin"
//handler: 处理函数
func (e *Echo) add(host, method, path string, handler HandlerFunc, middleware ...MiddlewareFunc) *Route {
//从reflect中获取函数名,使用runtime运行时指针指向的函数获取函数名
name := handlerName(handler)
//检查当前的host有没有对应的router,新建host的时候会创建和这个hostname对应的router,但是新建的host路由只能在group层
router := e.findRouter(host)
//加上这句可以保证echo.use和注册路由的先后顺序
//middleware = append(middleware, e.middleware...)
//加入字典树
router.Add(method, path, func(c Context) error {
h := applyMiddleware(handler, middleware...)
return h(c)
})
//创建新的路由结构体
r := &Route{
Method: method,
Path: path,
Name: name,
}
//map结构,方法和路径 用于唯一确定一个route,
//方法+路由 -> 处理的函数名
e.router.routes[method+path] = r
return r
}
5.创建中间件链结构
把中间件和处理函数组成chain
其中h作为最后一个执行的处理函数,同时h本身也可以作为一个链式结构的hanlder,只要形如:
func(c context) error{
h := applyMiddleware(hanlder,middleware)
return h©
}
middleware则按照声明顺序执行,通过倒序的遍历把hanlder函数传入middleware可以让每一个传入的hanlder作为next参数传入middlerware内的hanlder函数并形成链式结构
返回值作为第一个需要执行的hanlder其中的next函数和next函数的next函数…都已经赋值完成,通过h© 即可触发中间件的链式执行
func applyMiddleware(h HandlerFunc, middleware ...MiddlewareFunc) HandlerFunc {
for i := len(middleware) - 1; i >= 0; i-- {
h = middleware[i](h)
}
return h
}
6.路由匹配
// ServeHTTP implements `http.Handler` interface, which serves HTTP requests.
func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Acquire context
c := e.pool.Get().(*context)
c.Reset(r, w)
h := NotFoundHandler
if e.premiddleware == nil {
e.findRouter(r.Host).Find(r.Method, GetPath(r), c)
//这里的h就是注册路由的时候 存在 methodhandler映射表中的处理函数 是集成了 处理函数(可选)+路由中间件(可选)+组中间件(可选)
h = c.Handler()
h = applyMiddleware(h, e.middleware...)
} else {
h = func(c Context) error {
e.findRouter(r.Host).Find(r.Method, GetPath(r), c)
h := c.Handler()
h = applyMiddleware(h, e.middleware...)
return h(c)
}
h = applyMiddleware(h, e.premiddleware...)
}
// Execute chain
if err := h(c); err != nil {
e.HTTPErrorHandler(err, c)
}
// Release context
e.pool.Put(c)
}