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就好了文章来源:https://www.toymoban.com/news/detail-678059.html
完文章来源地址https://www.toymoban.com/news/detail-678059.html
到了这里,关于go web框架 gin-gonic源码解读03————middleware的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!