go web框架 gin-gonic源码解读03————middleware

go web框架 gin-gonic源码解读03————middleware(context)


今天打完游戏有空整理整理之前看的gin的中间件设计,go的中间件设计相较于前两站还是蛮简单,蛮容易看懂的,所以顺便把context也一起写一下。

中间件是现在web服务里统一化拓展最常用的功能,,他是为了在我们的web服务中实现一些可重复使用,可组合的功能方法、可以让我们的 web逻辑在执行之前或者之后进行预处理,后处理,验证等操作。 在说中间件之前,我们先回忆一下之前几张看过的代码的。
**gin.go** 文件中gin为了实现http.Handler接口,而实现的ServeHTTP()方法

// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	// 从对象池获取Context对象(对象池这里就不讲了,大家可以搜博客去看,这里只要大家会用就可以了)
	c := engine.pool.Get().(*Context)
	// 这三行其实就context的构造,传入http.ResponseWriter,*http.Request
	c.writermem.reset(w)
	c.Request = req
	c.reset()

	// 去我们的路由中查找响应的url并执行响应的逻辑
	engine.handleHTTPRequest(c)

	engine.pool.Put(c)
}

这里再顺便给大家介绍一下context,context实际上就是gin框架为了统一的参数管理包装的数据结构,他除了包含了我们每次访问ServeHTTP的传参(w http.ResponseWriter, req *http.Request)还包含了一些封装的web Response方法(调用就返回,非常好用),和一些请求获取参数的方法,其基本上覆盖了开发者平时所以操作需求,有兴趣的同学可以自己看看 .\Go\gin\context.go 中的内容。

// 这里缩略一下,只讲今天会用到的几个参数
type Context struct {
	writermem responseWriter
	Request   *http.Request
	Writer    ResponseWriter

	handlers HandlersChain 	// 中间件执行链handlers 
	index    int8					// 执行下标
	fullPath string

	// 引擎的指针
	engine       *Engine
	```
	略
	```
}

我们回到正题,看看我们的**handleHTTPRequest()**方法

// 去我们的路由中查找响应的url并执行响应的逻辑
engine.handleHTTPRequest( c )

这就是我们上次手撕的那个前缀树的查找方法。而我们的目的是通过收到http请求的url来找到,客户端需要请求的逻辑接口。

// 由于篇幅所限,代码会有所缩略

func (engine *Engine) handleHTTPRequest(c *Context) {
	// 很显然就是把我们请求方法和路径从Context里拿出来,方便使用
	httpMethod := c.Request.Method
	rPath := c.Request.URL.Path
	unescape := false
	```
	略
	```
	// Find root of the tree for the given HTTP method
	// 这里就是我们的前缀树的
	t := engine.trees
	for i, tl := 0, len(t); i < tl; i++ {
		// 这里前面说过的引擎(engine)中存的是一个前缀树的切片([]tree)
		//  每个请求方法一棵树,这里是遍历切片找到对应的请求方法
		if t[i].method != httpMethod {
			continue
		}
		root := t[i].root
		// Find route in tree
		// 找到树了,去树里查找
		value := root.getValue(rPath, c.params, c.skippedNodes, unescape)
		// 参数节点,拿参数
		if value.params != nil {
			c.Params = *value.params
		}
		// 关键点来了,这里就找到了我们要执行的逻辑方法
		if value.handlers != nil {
			c.handlers = value.handlers
			c.fullPath = value.fullPath
			c.Next()
			c.writermem.WriteHeaderNow()
			return
		}
	```
	略
	```
		}
		break
	}

	```
	略
	```
	serveError(c, http.StatusNotFound, default404Body)
}

为了讲的详细的点我们把我们的关键代码单独拿出来说

	c.handlers = value.handlers	// 1.将接口的handlers赋值给了Context的handlers
	c.fullPath = value.fullPath // 2.给fullPath 赋值
	c.Next()										// 3.执行Next()	方法
	c.writermem.WriteHeaderNow()// 4. 给HTTP response写入status code
	return

步骤1的赋值给的是handlers 而不是handler,这里大家可能会很奇怪,我业务逻辑其实一个函数就可以解决,这里为啥会缓存一个handlers呢,难道我的业务函数要拆分成好几个函数来写?其实这个handlers 存储的除了我们业务函数就是我们所有的中间件函数。

// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)

// HandlersChain defines a HandlerFunc slice.
type HandlersChain []HandlerFunc

gin的中间件调用的密码也都包含在步骤3里

func (c *Context) Next() {
	// 执行下标++
	c.index++
	// 一上来就自增是因为func (c *Context) reset()这个初始化方法里,会把下标初始化成-1,所以我们一上来就要++,让他变成0。
	// 这么做的目的就是因为我们的中间件函数中也会调用Next()
	for c.index < int8(len(c.handlers)) {
		// 通过下标去执行中间件
		c.handlers[c.index](c)
		// 显然执行完了++,不然就死循环了
		c.index++
	}
}

gin的中间件调用的秘密还是蛮简单的,接下来我们看看gin中间件的注册。
gin的中间件注册,大家都知道engine可以使用Use,路由组的RouterGroup也可以使用Use,实际上engine.Use()也是调用了RouterGroup.Use(),因为我们engine的路由包括了我们所有的RouterGroup的路由。

func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
	engine.RouterGroup.Use(middleware...)
	// 这边是初始化的时候顺便注册一些异常的处理方式
	// 当然为了避免重复的调用这两个rebuild40X函数,这个Use还是建议一次性调用到位
	engine.rebuild404Handlers()	
	engine.rebuild405Handlers()
	return engine
}

// Use adds middleware to the group, see example code in GitHub.
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
	// 非常的简单,我们的Handlers 是一个切片,他把所有的middleware插入到中间件的末端
	group.Handlers = append(group.Handlers, middleware...)
	return group.returnObj()
}

这样子就实现了我们功能强大的中间件调用链条,只说这些感觉内容少了点,再带大家看看gin自带的两个中间件把。

// 这是gin代码中获取默认Engine 的方法,大家一定都用过
func Default() *Engine {
	// 无关紧要的版本校验
	debugPrintWARNINGDefault()
	// New()第一篇应该说过
	engine := New()
	// 这里调用了两个中间件Logger(), Recovery()
	engine.Use(Logger(), Recovery())
	return engine
}

Logger() 顾名思义,很显然是打印日志的一个中间件,打印日志也是中间件在开发中最有用的几个用处之一

func Logger() HandlerFunc {
	return LoggerWithConfig(LoggerConfig{})
}

// LoggerWithConfig instance a Logger middleware with config.
func LoggerWithConfig(conf LoggerConfig) HandlerFunc {
	// LoggerConfig不是我们本文讲解的重点,大家可以自己去看看
	// 这下面的代码大意就是获取日志的打印格式和输出位置
	formatter := conf.Formatter
	if formatter == nil {
		formatter = defaultLogFormatter
	}

	out := conf.Output
	if out == nil {
		out = DefaultWriter
	}

	notlogged := conf.SkipPaths

	isTerm := true

	if w, ok := out.(*os.File); !ok || os.Getenv("TERM") == "dumb" ||
		(!isatty.IsTerminal(w.Fd()) && !isatty.IsCygwinTerminal(w.Fd())) {
		isTerm = false
	}

	// 知识点:struct{}不占内存空间
	// 在go语言map[string]struct{}的写法相当于创建了一个set结构体。
	var skip map[string]struct{}

	if length := len(notlogged); length > 0 {
		skip = make(map[string]struct{}, length)

		for _, path := range notlogged {
			skip[path] = struct{}{}
		}
	}
	// 正文开始
	return func(c *Context) {
		// Start timer
		start := time.Now()
		path := c.Request.URL.Path
		raw := c.Request.URL.RawQuery

		// Process request
		```
		// 可能很多人会好奇为什么在中间件的中间还要执行一个next
		// 这是一个中间件在逻辑函数执行之前还有逻辑函数执行之后,都有钩子逻辑可以执行
		// 大家可以把这个想象成一个套娃的结构
		// 例如:
		logger() 前半段
		logic()  业务逻辑代码
		logger() 后半段

		```
		c.Next()

		// Log only when path is not being skipped
		// 上面英文很简单大家自己看看
		if _, ok := skip[path]; !ok {
			param := LogFormatterParams{
				Request: c.Request,
				isTerm:  isTerm,
				Keys:    c.Keys,
			}

			// Stop timer
			// 结构化打印,然后输出到流中,没啥好说的
			param.TimeStamp = time.Now()
			param.Latency = param.TimeStamp.Sub(start)

			param.ClientIP = c.ClientIP()
			param.Method = c.Request.Method
			param.StatusCode = c.Writer.Status()
			param.ErrorMessage = c.Errors.ByType(ErrorTypePrivate).String()

			param.BodySize = c.Writer.Size()

			if raw != "" {
				path = path + "?" + raw
			}

			param.Path = path
			// 输出完了
			fmt.Fprint(out, formatter(param))
		}
	}
}

不过一般大家的项目之中都不会用gin提供的logger中间件,因为大家都有自己的日志格式,不过你实在想用也可以,把自己的logger实例实现gin.logger的接口,然后在初始化engine的时候传给engine就好了

你可能感兴趣的:(Go,gin源码,golang,gin,go,后端)