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个方法。

参考:

posted on 2022-12-09 16:44  进击的davis  阅读(190)  评论(0编辑  收藏  举报

导航