思考(五十四):Golang 编程之责任链模式

责任链模式

责任链模式:在处理核心业务前后,可能会有很多道自定义的工序,每道工序间存在线性依赖关系。简单粗暴的,是所有代码揉成一团

责任链模式使得每道工序可以自由拼接,传递顺序明确,便于扩展

典型的应用是 http 请求处理, github 上有一个项目使用了该模式,网址如下:

https://github.com/gin-gonic/gin#using-middleware

下面先给一个感性的例子,再详细分析下其实现原理

gin 使用例子展示

这里引用 bilibili/discovery 中使用 gin 的例子:

(摘录部分代码,增加注释、删除干扰代码等)

func Init(...) {
	engine := gin.New()                                        // 实例化 gin
	engine.Use(loggerHandler, recoverHandler)                  // 注册中间件
	innerRouter(engine)                                        // 注册 HTTP 请求处理函数
	go func() {
		if err := engine.Run(c.HTTPServer.Addr); err != nil {  // 开始监听端口,提供 HTTP 服务
			panic(err)
		}
	}()
}

// 注册 HTTP 请求处理函数
func innerRouter(e *gin.Engine) {
	group := e.Group("/discovery")
	group.POST("/register", register)
	group.POST("/renew", renew)
}

// 工序1:打印日志,包括 耗时、错误号、IP信息等 
func loggerHandler(c *gin.Context) {
	// Start timer
	start := time.Now()
	path := c.Request.URL.Path
	raw := c.Request.URL.RawQuery
	method := c.Request.Method

	// Process request
	c.Next()

	// Stop timer
	end := time.Now()
	latency := end.Sub(start)
	statusCode := c.Writer.Status()
	ecode := c.GetInt(contextErrCode)
	clientIP := c.ClientIP()
	if raw != "" {
		path = path + "?" + raw
	}
	log.Infof("METHOD:%s | PATH:%s | CODE:%d | IP:%s | TIME:%d | ECODE:%d", method, path, statusCode, clientIP, latency/time.Millisecond, ecode)
}

// 工序2:异常捕获,打印异常堆栈信息、并应答 500 错误号
func recoverHandler(c *gin.Context) {
	defer func() {
		if err := recover(); err != nil {
			const size = 64 << 10
			buf := make([]byte, size)
			buf = buf[:runtime.Stack(buf, false)]
			httprequest, _ := httputil.DumpRequest(c.Request, false)
			pnc := fmt.Sprintf("[Recovery] %s panic recovered:\n%s\n%s\n%s", time.Now().Format("2006-01-02 15:04:05"), string(httprequest), err, buf)
			fmt.Fprintf(os.Stderr, pnc)
			log.Error(pnc)
			c.AbortWithStatus(500)
		}
	}()
	c.Next()
}

粗看这段代码,刚开始我是想不通的,loggerHandler 、recoverHandler 、与具体的业务逻辑 register 、renew ,都是独立的函数,如何做到让 loggerHandler 能监测到耗时、 recoverHandler 做到异常捕获呢?

看了下 gin 代码即可理解,其核心在于 c.Next() 方法

实现原理分析

上述代码,1 个 HTTP 请求进来,实际执行过程,函数调用栈如下:

函数调用栈 ----->
+----------------+ +----------------+ +----------------+ +----------------+ +----------------+
|  1             | |  2             | |  3             | |  4             | |  5             |
|                | |                | |                | |                | |                |
|                | |                | |                | |                | |                |
|                | |                | |                | |                | |                |
|                | |                | |    register    | |                | |                |
|                | |                | +----------------+ |                | |                |
|                | |                | |     c.Next     | |                | |                |
|                | +----------------+ +----------------+ +----------------+ |                |
|                | | recoverHandler | | recoverHandler | | recoverHandler | |                |
|                | +----------------+ +----------------+ +----------------+ |                |
|                | |     c.Next     | |     c.Next     | |     c.Next     | |                |
+----------------+ +----------------+ +----------------+ +----------------+ +----------------+
| loggerHandler  | | loggerHandler  | | loggerHandler  | | loggerHandler  | | loggerHandler  |
+----------------+ +----------------+ +----------------+ +----------------+ +----------------+
|     c.Next     | |     c.Next     | |     c.Next     | |     c.Next     | |     c.Next     |
+----------------+ +----------------+ +----------------+ +----------------+ +----------------+

有上图可以看出:

扫描二维码关注公众号,回复: 8631920 查看本文章
  • 业务逻辑 register 在 loggerHandler 函数内的 c.Next() 函数内执行,因此 loggerHandler 可以计算耗时
  • 业务逻辑 register 在 recoverHandler 函数内的 c.Next() 函数内执行,因此 recoverHandler 可以捕获异常

因此不难猜测:

  • engine.Use 、 engine.Group.POST 等方法的作用,就是注册句柄时,顺便把这些句柄组织成函数链(责任链)

gin 代码分析

1. 组织责任链

组织责任链相关代码均在 https://github.com/gin-gonic/gin/blob/master/routergroup.go

摘录主要相关代码如下,并添加注释:

  • gin.Engine ,组合嵌入 RouterGroup,因此具有 RouterGroup 的所有属性

    type Engine struct {
    	RouterGroup
    }
    
  • RouterGroup.Use ,把 middleware (责任)按序保存到 Handlers 字段

    // 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()
    }
    
  • RouterGroup.POST ,把业务逻辑加到 middleware 链(责任链)尾部,并正式注册路由

    (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes {
    	return group.handle("POST", relativePath, handlers)
    }
    func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
    	absolutePath := group.calculateAbsolutePath(relativePath)
    	handlers = group.combineHandlers(handlers)                         // 将业务句柄 handlers 加到 middleware 链尾部
    	group.engine.addRoute(httpMethod, absolutePath, handlers)          // 这里才是真正注册句柄(该句柄为 middleware 链 + 业务句柄 handlers )
    	return group.returnObj()
    }
    func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
    	finalSize := len(group.Handlers) + len(handlers)
    	if finalSize >= int(abortIndex) {
    		panic("too many handlers")
    	}
    	mergedHandlers := make(HandlersChain, finalSize)
    	copy(mergedHandlers, group.Handlers)
    	copy(mergedHandlers[len(group.Handlers):], handlers) // 将业务句柄 handlers 加到 middleware 链尾部
    	return mergedHandlers
    }
    
  • RouterGroup.Group ,与责任链模式无关代码,仅为代码阅读好看些,用于保存 URL 请求的公共部分

    func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
    	return &RouterGroup{
    		Handlers: group.combineHandlers(handlers),
    		basePath: group.calculateAbsolutePath(relativePath), // 保存 URL 请求公共部分
    		engine:   group.engine,
    	}
    }
    
2. 执行

执行责任链相关代码均在: https://github.com/gin-gonic/gin/blob/master/gin.go

摘录主要相关代码如下,并添加注释:

  • Engine.addRoute ,按 URL 请求路径,维护树结构,方便查找 责任链 。细节可不深究,与本文无关

    func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
    	assert1(path[0] == '/', "path must begin with '/'")
    	assert1(method != "", "HTTP method can not be empty")
    	assert1(len(handlers) > 0, "there must be at least one handler")
    
    	debugPrintRoute(method, path, handlers)
    	root := engine.trees.get(method)
    	if root == nil {
    		root = new(node)
    		root.fullPath = "/"
    		engine.trees = append(engine.trees, methodTree{method: method, root: root})
    	}
    	root.addRoute(path, handlers)
    }
    
  • Engine.handleHTTPRequest ,执行责任链,看 2 行中文注释即可,细节可不深究,与本文无关

func (engine *Engine) handleHTTPRequest(c *Context) {
	httpMethod := c.Request.Method
	rPath := c.Request.URL.Path
	unescape := false
	if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
		rPath = c.Request.URL.RawPath
		unescape = engine.UnescapePathValues
	}
	rPath = cleanPath(rPath)

	// Find root of the tree for the given HTTP method
	t := engine.trees
	for i, tl := 0, len(t); i < tl; i++ {
		if t[i].method != httpMethod {
			continue
		}
		root := t[i].root
		// Find route in tree
		value := root.getValue(rPath, c.Params, unescape)         // 根据 URL 请求路径,获取`责任链`
		if value.handlers != nil {
			c.handlers = value.handlers
			c.Params = value.params
			c.fullPath = value.fullPath
			c.Next()                                              // 开始执行`责任链`
			c.writermem.WriteHeaderNow()
			return
		}
		if httpMethod != "CONNECT" && rPath != "/" {
			if value.tsr && engine.RedirectTrailingSlash {
				redirectTrailingSlash(c)
				return
			}
			if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
				return
			}
		}
		break
	}

	if engine.HandleMethodNotAllowed {
		for _, tree := range engine.trees {
			if tree.method == httpMethod {
				continue
			}
			if value := tree.root.getValue(rPath, nil, unescape); value.handlers != nil {
				c.handlers = engine.allNoMethod
				serveError(c, http.StatusMethodNotAllowed, default405Body)
				return
			}
		}
	}
	c.handlers = engine.allNoRoute
	serveError(c, http.StatusNotFound, default404Body)
}

以上

发布了129 篇原创文章 · 获赞 73 · 访问量 16万+

猜你喜欢

转载自blog.csdn.net/u013272009/article/details/91344594