
理解 Go Web 框架中间件Next、路由分组与执行链最近在跟着 Gee Web Framework 教程学习 Go Web 框架的实现。今天学习的是Day5中间件 Middleware。这一部分代码量并不算大但把之前学过的很多 Go 知识串了起来例如函数类型可变参数切片与append路由分组strings.HasPrefix函数调用栈中间件执行链HTTP 状态码请求耗时统计在实现过程中我主要遇到了几个问题Next()为什么能够实现中间件的嵌套执行...HandlerFunc中的三个点是什么意思strings.HasPrefix()在路由分组中起什么作用Fail()是如何阻止后续 Handler 执行的time.Since()如何以µs为单位显示为什么log.Print()中的%s没有被格式化下面对今天学习的内容进行整理。一、中间件是什么在 Web 框架中中间件可以理解为在真正执行请求处理函数之前或之后额外执行的一段公共逻辑。常见用途包括日志记录用户鉴权跨域处理异常恢复请求耗时统计限流例如 Gee 中的 LoggerfuncLogger()HandlerFunc{returnfunc(c*Context){// Start timet:time.Now()// Process requestc.Next()// Calculate resolution timelog.Printf([%d] %s in %v,c.StatusCode,c.Req.RequestURI,time.Since(t),)}}这里最重要的是c.Next()Logger 并不是自己处理最终请求而是先执行自己的前置逻辑再通过Next()把请求交给后面的 Handler。等后面的 Handler 执行完成以后再回来继续执行Next()后面的代码。因此 Logger 可以实现记录开始时间 ↓ 执行后续 Handler ↓ Handler 执行结束 ↓ 计算总耗时 ↓ 打印日志二、HandlerFunc 本质上是一个函数类型Gee 中定义typeHandlerFuncfunc(*Context)这表示任何参数为*Context、没有返回值的函数都可以作为HandlerFunc。例如func(c*Context){c.HTML(200,h1Hello Gee/h1)}它符合func(*Context)因此可以作为路由 Handler。中间件最终返回的同样也是一个HandlerFuncfuncLogger()HandlerFunc{returnfunc(c*Context){// ...}}所以我们可以写r.Use(gee.Logger())把 Logger 注册为中间件。三、...HandlerFunc中的三个点是什么意思Gee 中注册中间件的代码func(group*RouterGroup)Use(middlewares...HandlerFunc){group.middlewaresappend(group.middlewares,middlewares...,)}这里出现了两个...。虽然长得一样但含义稍有不同。1. 参数定义中的...middlewares...HandlerFunc这里表示middlewares是一个可变参数可以传入任意数量的HandlerFunc。例如r.Use(Logger())可以。也可以一次传多个r.Use(Logger(),Recovery(),Auth())甚至可以r.Use()进入函数内部以后middlewares实际上就是[]HandlerFunc也就是一个切片。可以理解成 Go 帮我们把传入的多个参数收集成了一个切片。例如functest(nums...int){fmt.Println(nums)}调用test(1,2,3)函数内部的nums就是[1 2 3]2.append中的...再看append(group.middlewares,middlewares...)这里的middlewares...表示把切片中的元素展开一个一个传入。例如a:[]int{1,2}b:[]int{3,4,5}aappend(a,b...)最终[1 2 3 4 5]可以把append(a,b...)想象成append(a,b[0],b[1],b[2])因此这两个...可以这样记func f(args ...T) ↑ 收集多个参数 f(slice...) ↑ 展开切片四、路由分组如何选择中间件Gee 支持 RouterGroup例如v2:r.Group(/v2)然后v2.Use(onlyForV2())表示onlyForV2只属于/v2这个路由组。服务器收到请求以后会在ServeHTTP()中遍历所有路由组func(engine*Engine)ServeHTTP(w http.ResponseWriter,req*http.Request,){varmiddlewares[]HandlerFuncfor_,group:rangeengine.groups{ifstrings.HasPrefix(req.URL.Path,group.prefix,){middlewaresappend(middlewares,group.middlewares...,)}}c:newContext(w,req)c.handlersmiddlewares engine.router.handle(c)}这里最重要的是strings.HasPrefix(req.URL.Path,group.prefix,)五、strings.HasPrefix()是什么Go 标准库提供strings.HasPrefix(s,prefix)作用是判断字符串s是否以prefix开头。例如strings.HasPrefix(/v2/hello/yuexi,/v2,)结果true而strings.HasPrefix(/v1/hello,/v2,)结果false因此ifstrings.HasPrefix(req.URL.Path,group.prefix,)实际上是在判断当前请求是否属于这个 RouterGroup。例如当前请求/v2/hello/yuexi假设存在两个 group根 group prefix v2 group prefix /v2对于根 groupstrings.HasPrefix(/v2/hello/yuexi,,)结果true所以全局中间件会加入执行链。对于/v2strings.HasPrefix(/v2/hello/yuexi,/v2,)结果同样为true因此/v2的中间件也会加入。最终可能得到middlewares [ Logger, onlyForV2, ]六、Context 中为什么需要 handlers 和 index为了支持中间件Context中增加了typeContextstruct{// ...handlers[]HandlerFunc indexint}其中handlers保存当前请求需要执行的所有 Handler。例如[ Logger, Auth, Handler, ]而index表示当前 Handler 链执行到了哪个位置。创建 Context 时funcnewContext(w http.ResponseWriter,req*http.Request,)*Context{returnContext{Path:req.URL.Path,Method:req.Method,Req:req,Writer:w,index:-1,}}这里index:-1非常重要。因为第一次执行c.Next()会先c.index于是-1 1 0刚好从handlers[0]开始执行。七、最关键的 Next()Gee 中func(c*Context)Next(){c.indexs:len(c.handlers)for;c.indexs;c.index{c.handlers[c.index](c)}}这段代码是整个中间件机制的核心。1. index 可以理解为游标假设handlers: [Logger, Auth, Handler] 0 1 2初始状态index -1第一次c.Next()执行c.index得到index 0于是执行c.handlers[0](c)也就是LoggerLogger 中又调用c.Next()这时index 0再次c.index得到index 1于是执行AuthAuth 如果又调用c.Next()则index 2继续执行Handler所以index本质上是在做把处理链的游标移动到下一个 Handler。八、为什么 Next 能实现洋葱模型假设有两个中间件funcA(c*Context){fmt.Println(A1)c.Next()fmt.Println(A2)}以及funcB(c*Context){fmt.Println(B1)c.Next()fmt.Println(B2)}最终 HandlerfuncHandler(c*Context){fmt.Println(Handler)}Handler 链[A, B, Handler]最终打印A1 B1 Handler B2 A2为什么因为调用过程实际上是A │ ├── A1 │ └── Next() │ ▼ B │ ├── B1 │ └── Next() │ ▼ Handler │ ▼ 返回 │ └── B2 │ └── A2也就是说请求进入 ↓ A 前置逻辑 ↓ B 前置逻辑 ↓ Handler ↓ B 后置逻辑 ↓ A 后置逻辑 ↓ 请求结束这就是常说的洋葱模型。这里并没有什么特殊的 Go 语法。真正实现这个效果的是index 控制 Handler 执行位置 普通的 Go 函数调用栈九、路由 Handler 为什么也要加入 handlers在之前的实现中找到路由 Handler 后可能直接handler(c)但有了中间件以后需要把所有 Handler 放进同一个执行链。所以现在func(r*router)handle(c*Context){n,params:r.getRoute(c.Method,c.Path,)ifn!nil{c.Paramsparams key:c.Method-n.pattern c.handlersappend(c.handlers,r.handlers[key],)}else{c.handlersappend(c.handlers,func(c*Context){c.String(http.StatusNotFound,404 NOT FOUND: %s\n,c.Path,)},)}c.Next()}假设ServeHTTP()已经收集到[ Logger, Auth, ]router.handle()再把最终路由函数加入[ Logger, Auth, Handler, ]最后统一c.Next()因此Middleware Middleware Handler全部被放到了同一条执行链中。十、Fail() 是如何终止后续 Handler 的在/v2中间件中funconlyForV2()gee.HandlerFunc{returnfunc(c*gee.Context){t:time.Now()c.Fail(500,Internal Server Error,)log.Printf([%d] %s in %v for group v2,c.StatusCode,c.Req.RequestURI,time.Since(t),)}}其中调用了c.Fail(500,Internal Server Error)Fail()的实现func(c*Context)Fail(codeint,errstring,){c.indexlen(c.handlers)c.JSON(code,H{message:err,})}最关键的是c.indexlen(c.handlers)举个例子假设handlers [ Logger, onlyForV2, Handler, ]对应0 Logger 1 onlyForV2 2 Handler现在执行onlyForV2所以index 1调用c.Fail(...)之后c.indexlen(c.handlers)也就是index 3随后返回Next()for;c.indexs;c.index{判断3 3结果false所以Handler不会继续执行。Fail 和 return 不一样需要注意c.Fail(...)并不会直接终止当前函数。例如c.Fail(500,Internal Server Error,)log.Printf(hello)这里的log.Printf(hello)仍然会执行。所以return表示结束当前函数。而c.Fail()表示阻止Next()继续执行后面的 Handler。这是两件不同的事情。十一、全局中间件与分组中间件的执行过程当前代码r.Use(gee.Logger())注册的是全局中间件。然后v2:r.Group(/v2)v2.Use(onlyForV2())注册的是/v2路由组中间件。如果访问/v2/hello/yuexi最终 Handler 链大致是[ Logger, onlyForV2, /v2/hello/:name Handler, ]执行过程Logger │ ├── 开始计时 │ └── c.Next() │ ▼ onlyForV2 │ ├── 开始计时 │ ├── c.Fail() │ │ │ ├── index len(handlers) │ └── 返回 500 JSON │ └── 打印 v2 日志 │ ▼ 回到 Logger │ └── 打印全局日志由于c.Fail()修改了index所以真正的/v2/hello/:name Handler不会执行。因此可以看到类似[500] /v2/hello/yuexi in xxx for group v2 [500] /v2/hello/yuexi in xxx这也体现出了全局中间件 路由组中间件是可以组合使用的。十二、time.Since 为什么会显示成 0sLogger 中使用t:time.Now()c.Next()log.Printf([%d] %s in %v,c.StatusCode,c.Req.RequestURI,time.Since(t),)有时会看到[200] / in 0s因为当前 Handler 非常简单执行速度可能非常快。如果想统一使用微秒µs可以使用time.Since(t).Microseconds()例如log.Printf([%d] %s in %dµs,c.StatusCode,c.Req.RequestURI,time.Since(t).Microseconds(),)输出可能是[200] / in 5µs更精确地显示微秒需要注意Microseconds()返回的是整数。如果实际执行时间不足1µs那么仍然可能得到0µs如果想显示小数可以elapsed:time.Since(t)log.Printf([%d] %s in %.3fµs,c.StatusCode,c.Req.RequestURI,float64(elapsed)/float64(time.Microsecond),)例如[200] / in 3.142µs这里float64(elapsed)/float64(time.Microsecond)相当于Duration ↓ 换算成微秒对于这种非常短的 Handler更方便观察执行时间。十三、log.Print 和 log.Printf 的区别启动项目时还遇到了这样一个输出Route %4s - %sGET/ Route %4s - %sGET/v2/hello/:name本来希望%4s %s被对应的数据替换。代码写的是log.Print(Route %4s - %s,method,pattern,)问题在于使用了log.Print()Print()不会按照格式化占位符处理%s %d %v如果需要格式化字符串应使用log.Printf()所以应该改成log.Printf(Route %4s - %s,method,pattern,)最终可以输出Route GET - / Route GET - /v2/hello/:name因此可以简单记log.Print(...)用于普通输出。log.Printf(...)用于格式化输出。例如log.Printf(name%s age%d,name,age,)十四、把整个中间件流程串起来今天学习的代码串起来以后大致就是r.Use(Logger()) │ ▼ RouterGroup.middlewares │ ▼ engine.groups │ │ ServeHTTP 遍历 ▼ strings.HasPrefix() │ ▼ 找到当前 URL 对应的 Middleware │ ▼ middlewares │ ▼ c.handlers │ │ router.handle() ▼ 追加真正的路由 Handler │ ▼ [ Middleware1, Middleware2, Handler, ] │ ▼ c.Next() │ ▼ index 不断向后移动 │ ▼ 依次执行 Handler其中最重要的是RouterGroup负责管理哪些中间件属于哪些路由。handlers负责保存当前请求实际需要执行的处理函数。index负责保存当前 Handler 执行到了哪里。Next()负责推进 Handler 执行链。最终再结合 Go 自身的函数调用栈实现中间件前置逻辑 ↓ c.Next() ↓ 后续 Middleware ↓ Handler ↓ 中间件后置逻辑十五、总结Gee Day5 的代码量并不算大但这一部分对于理解 Web 框架非常重要。今天最大的收获不是单纯学会c.Next()怎么写而是理解了整个中间件链是怎么建立并运行起来的注册 Middleware ↓ 保存到 RouterGroup ↓ ServeHTTP 根据 URL 收集中间件 ↓ 写入 Context.handlers ↓ 追加最终路由 Handler ↓ 调用 c.Next() ↓ 通过 index 推进处理链 ↓ 通过函数调用栈形成洋葱模型几个今天需要记住的知识点1. 可变参数middlewares...HandlerFunc表示接收多个HandlerFunc。2. 展开切片middlewares...表示把切片展开成一个个元素。3. 判断字符串前缀strings.HasPrefix(path,prefix,)可以判断一个请求是否属于某个 RouterGroup。4. Handler 执行位置c.index相当于把 Handler 链的游标向后移动。5. 执行后续 Handlerc.Next()会继续执行 Handler 链中剩余的处理函数。6. 中止后续处理链c.Fail(...)通过c.indexlen(c.handlers)让后面的 Handler 不再执行。7. 微秒耗时time.Since(t).Microseconds()可以把请求耗时以µs为单位输出。8. 格式化日志需要使用格式占位符时log.Printf(...)而不是log.Print(...)通过 Gee Day5我对 Go Web 框架中的中间件机制有了更清楚的认识。之前看到c.Next()可能只是知道“它会执行下一个中间件”。现在再看这段代码func(c*Context)Next(){c.indexs:len(c.handlers)for;c.indexs;c.index{c.handlers[c.index](c)}}就能够理解所谓中间件执行链本质上就是一个HandlerFunc切片加上一个用于记录当前位置的index再利用普通的函数调用栈实现前置逻辑和后置逻辑。这也是今天学习 Gee Day5 最重要的收获。