gin源码学习-中间件解析(3)
1.net/http中间件
net/http部分参照-Go 每日一库之 net/http(基础和中间件),看原文请移步:https://cloud.tencent.com/developer/article/1852023
标准库net/http中并没有实现中间件这种功能,net/http为我们提供的是一种基本能用,但生产不行的接口,但我们可以基于golang的闭包及net/http的接口去实现中间件。
通常我们用net/http写个测试的demo类似下面的:
package main
import (
"log"
"net/http"
)
func main() {
// v1.0 不含中间的路由
//http.HandleFunc("/ping", pong)
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatalln("start http server fail: ", err)
}
}
这种路由写着看起来简单,实际上对于尽量复用代码的通用前置后置处理来说简直痛苦,你得每个路由handler里面都要去实现诸如日志记录,请求耗时记录等,基于这种痛点,就产生了通用的中间件的需求。
写中间件之前,先考虑这样的问题,在net/http的handler的实现中,怎样去实现中间件呢?
net/http中,我们是这样注册路由的:
// HandleFunc registers the handler function for the given pattern.
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if handler == nil {
panic("http: nil handler")
}
mux.Handle(pattern, HandlerFunc(handler))
}
func (mux *ServeMux) Handle(pattern string, handler Handler) {
mux.mu.Lock()
defer mux.mu.Unlock()
...
if mux.m == nil {
mux.m = make(map[string]muxEntry)
}
e := muxEntry{h: handler, pattern: pattern}
mux.m[pattern] = e
if pattern[len(pattern)-1] == '/' {
mux.es = appendSorted(mux.es, e)
}
...
}
通过调用mux.HandleFunc(path, func(ResponseWriter, *Request))
,其实也就是调用mux.Handle(pattern string, handler Handler)
来实现路由信息的注册,HandleFunc可以认为是控制器函数,而Handle就是控制器,看看Handle上做的处理吧。
v2.0
package main
import (
"log"
"net/http"
"time"
)
func main() {
// v2.0 带中间件的路由
http.Handle("/ping", weblog(timeRecord(http.HandlerFunc(pong))))
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatalln("start http server fail: ", err)
}
}
func pong(w http.ResponseWriter, r *http.Request) {
log.Printf("url: %s call now", r.URL.Path)
w.Write([]byte("pong"))
}
func weblog(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("url: %s process start...\n", r.URL.Path)
defer func() {
log.Printf("url: %s process end...\n", r.URL.Path)
}()
handler.ServeHTTP(w, r)
})
}
func timeRecord(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
defer func() {
log.Printf("url: %s cost: %f s...\n", r.URL.Path, time.Since(start).Seconds())
}()
time.Sleep(time.Second)
handler.ServeHTTP(w, r)
})
}
通过看HandleFunc和Handle的方法签名可以看出,结合golang的闭包,我们在Handle中的handler是返回接口-gin.Handler不就是可以了吗。
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
以timeRecord函数为例,传入gin.Handler类型,返回gin.Handler类型,在返回函数里,我们把真正的控制器函数通过ServeHTTP调用处理请求,而且也实现了中间件的功能,如果要记录web信息,再把timeRecord传入weblog函数即可。
来看看处理后的结果怎样:
从图中可以看出,请求依次经过weblog > timeRecord > pong > timeRecord > weblog
,也符合中间件的处理顺序。
虽然但是,有个问题,难道每次我们都要这样写吗?
那就再写个处理函数吧。
v3.0
看修改部分
// func main
//v3.0 带handle的中间件的路由
http.Handle("/ping", applyMiddleware(http.HandlerFunc(pong), weblog, timeRecord))
// applyMiddleware
type Middleware func(handler http.Handler) http.Handler
func applyMiddleware(handler http.Handler, middleware ...Middleware) http.Handler {
// 以参数形式传入handler中,注意传参顺序,倒序传入
for i := len(middleware)-1; i >= 0; i-- {
handler = middleware[i](handler)
}
return handler
}
能否更简单呢?
v4.0
type greeting string
func (g greeting) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello"))
}
func main() {
mu := NewMyMux()
// 注意顺序,先调放前面
middleware := []Middleware{
weblog,
timeRecord,
}
mu.Use(middleware...)
mu.HandleFunc("/ping", pong)
mu.Handle("/", greeting("hello"))
server := &http.Server{
Addr: ":8080",
Handler: mu,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
}
_ = server.ListenAndServe()
}
type MyMux struct {
*http.ServeMux
middleware []Middleware
}
func NewMyMux() *MyMux {
return &MyMux{
ServeMux: http.NewServeMux(),
middleware: nil,
}
}
func (m *MyMux) Use(middleware ...Middleware) {
m.middleware = append(m.middleware, middleware...)
}
func (m *MyMux) Handle(pattern string, handler http.Handler) {
// 闭包,调用net/http的ServeMux的方法,做一层封装
handler = applyMiddleware(handler, m.middleware...)
m.ServeMux.Handle(pattern, handler)
}
func (m *MyMux) HandleFunc(pattern string, handler http.HandlerFunc) {
newHandler := applyMiddleware(handler, m.middleware...)
m.ServeMux.Handle(pattern, newHandler)
}
// 参照http.ServeMux, 改写ServeHTTP
func (m *MyMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.RequestURI == "*" {
if r.ProtoAtLeast(1, 1) {
w.Header().Set("Connection", "close")
}
w.WriteHeader(http.StatusBadRequest)
return
}
h, _ := m.Handler(r)
h.ServeHTTP(w, r)
}
运行后,结果一致,老铁没毛病。
2.gin中间件
中间件直接说源码优点枯燥,不妨从demo来以小窥大。
package main
import (
"log"
"net/http"
"time"
"github.com/gin-gonic/gin"
)
func main() {
engine := gin.New()
server := http.Server{
Addr: ":8080",
// called handler, if nil default http.DefaultServeMux
Handler: engine,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
}
// 注册全局中间件
// recovery中间件, 记录日志中间件, 请求处理时间耗时中间件
engine.Use(gin.Recovery(), webLogMiddleware(), timeRecordMiddleware())
// 注册组中间件
// 除注册用户外,其余组内路由访问需要认证的中间件
// 方法1,组内加中间件
group1 := engine.Group("/app/v1", authRequiredMiddleware())
// 方法2,组内Use()加中间件
// 通过组路由Use()方法添加中间件
// group1.Use(authRequiredMiddleware())
{
group1.POST("/add", addUser)
group1.GET("/get", getUser)
group1.DELETE("/delete", deleteUser)
}
// 单个组路添加中间件
engine.GET("/ping", myMiddleware(), pong)
err := server.ListenAndServe()
if err != nil {
log.Fatalln("server start failed:", err)
}
}
func pong(ctx *gin.Context) {
ctx.JSON(http.StatusOK, gin.H{
"msg": "pong",
})
}
func addUser(ctx *gin.Context) {
// 具体业务逻辑...
ctx.JSON(http.StatusOK, gin.H{
"msg": "add user success.",
})
}
func getUser(ctx *gin.Context) {
// 具体业务逻辑...
ctx.JSON(http.StatusOK, gin.H{
"msg": "get user info success.",
"username": "Alice",
"address": "Tianfu 1st",
})
}
func deleteUser(ctx *gin.Context) {
// 具体业务逻辑...
ctx.JSON(http.StatusOK, gin.H{
"msg": "delete user success.",
})
}
// 时间耗时中间件
func timeRecordMiddleware() gin.HandlerFunc {
return func(ctx *gin.Context) {
start := time.Now()
ctx.Next()
// 模拟请求处理耗时
time.Sleep(2 * time.Second)
cost := time.Since(start).Seconds()
log.Printf("request handle time cost: %f\n", cost)
}
}
// 记录日志中间件
func webLogMiddleware() gin.HandlerFunc {
return func(ctx *gin.Context) {
log.Println("before handler, extract response.")
ctx.Next()
log.Println("after handler, record request/response here.")
}
}
// 认证的中间件
func authRequiredMiddleware() gin.HandlerFunc {
return func(ctx *gin.Context) {
skipedPath := "/app/v1/add"
path := ctx.Request.URL.Path
// auth only exclude user add api
if path != skipedPath {
// 具体业务逻辑
}
ctx.Next()
}
}
// 中间件
func myMiddleware() gin.HandlerFunc {
return func(ctx *gin.Context) {
log.Println("balabala...")
ctx.Next()
}
}
上面的demo包含的中间件类型主要是三种:
- 1.全局中间件
- 2.组内中间件
- 3.单个路由中间件
接下来按此顺序顺着源码看看。
2.1 全局中间件
在创建了gin的engine后,我们就可以通过engine.Use()
方法添加中间件,以下是engine.Use()
方法源码。
// Use attaches a global middleware to the router. i.e. the middleware attached through Use() will be
// included in the handlers chain for every single request. Even 404, 405, static files...
// For example, this is the right place for a logger or error management middleware.
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
// 调用RouterGroup的Use()方法
engine.RouterGroup.Use(middleware...)
...
return engine
}
从源码看到,实际用engine.Use()
方法,其实也是调用RouterGroup.Use()
方法,从入参来看,传入的中间件其实也是可变的middleware,即可以一个一个middleware传入,也可以切片传入。
全局中间件注册以后,对全局路由都适用,通常我们会在全局上添加一些如请求耗时、日志记录、panic-recovery的中间件,方便记录与恢复整体service。
2.2 组内中间件
源码:
// Use adds middleware to the group, see example code in GitHub.
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
group.Handlers = append(group.Handlers, middleware...)
return group.returnObj()
}
源码处理也不复杂,直接将添加的组内路由加到已有中间件的尾部,这里需要注意的地方是,如果是组内路由,在group的时候,其实会新建个group的,所以组内添加的中间件只对组内适用,来看看RouterGroup.Group()
方法源码。
// Group creates a new router group. You should add all the routes that have common middlewares or the same path prefix.
// For example, all the routes that use a common middleware for authorization could be grouped.
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
return &RouterGroup{
// 复制一份全局的中间件handlers,即处理后的handlers = 全局中间件+组内中间件
Handlers: group.combineHandlers(handlers),
// 计算根path
basePath: group.calculateAbsolutePath(relativePath),
// 引用同一个engine 实例
engine: group.engine,
}
}
上面方法是在Group()
方法中添加中间件,涉及的中间中间件处理就是复制一份全局中间件的handlers,加上添加的组内中间件handlers,在新建的RouterGroup实例中设置handlers = 全局中间件+组内中间件。
当然,在RouterGroup
中也提供`Use()`方法,我们来看看。
// Use adds middleware to the group, see example code in GitHub.
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
group.Handlers = append(group.Handlers, middleware...)
return group.returnObj()
}
可以看到,其实就是在组内的现有的中间件handlers的基础上,再加上新添加的handlers,这个组内可以认为是全局的范围,也可以认为是确实就是组内的范围,取决于是否新建了组路由,默认下就是全局的,新建了组路由就是组内的,依然很简单不是。
2.3 单个路由中间件
单个路由中间件,仅适用于该路由,为什么这样说,且看源码。
还以demo为例,我们在添加"/ping"
路由方法为GET
,看源码:
// GET is a shortcut for router.Handle("GET", path, handle).
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
// 调用handle()方法
return group.handle(http.MethodGet, relativePath, handlers)
}
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
absolutePath := group.calculateAbsolutePath(relativePath)
// 组内中间件handlers + 添加的handlers(最后一个为真正的业务逻辑处理的控制器handler)
handlers = group.combineHandlers(handlers)
// 添加路由信息
group.engine.addRoute(httpMethod, absolutePath, handlers)
return group.returnObj()
}
根据之前在路由注册中学习,我们知道,注册路由时,实际gin是将路由信息添加到方法树中。
做个小结:
全局中间件
gin的engine实例维护一个全局的handlers,即:engine.RouterGroup.Handlers
,添加路由时把这个加到路由的handlers,然后插入到路由存储数据结构方法树中。
组内中间件
每次新建组路由,都会新建个RouterGroup实例
,将engine维护的handlers(全局中间件)复制一份,然后加上组内路由,组建成一个新的handlers,即:engine.RouterGroup.Handlers + handlers(组路由上添加的中间件)
,待创建具体的路由时处理。
单个路由中间件
新建具体的路由时,在路由中添加中间件handlers,会加上group.Handlers,即该路由对应的handlers = group.Handlers + handlers(单个路由添加的handlers)
,每个路由的handlers都是如此处理,实际注册路由即加到方法树中。举个例子,group.Handlers = Handlers,路由1:path-1,添加的handlers为handlers_1,则插入到对应方法树中的路由信息应该是:path-1 和 Handlers + handlers_1
。
总体来说,gin对中间件的处理比较简单,不管是全局中间件、组内中间件、单个路由中间件,最终都是要插入到方法树中维护路由信息,engine在新建实例时,就创建了9棵methodTree
。为什么是9呢,因为http method就是get/post/put/patch/head/options/delete/connect/trace
,一共9个方法。
参考: