如何用 Go 语言写出好用的 Http 中间件?

当我们用 Go 语言编写较为复杂的服务时,一个永恒的话题就是中间件。这个话题在网上被 一遍一遍又一遍 地讨论着。归根结底,中间件应该允许我们:

  1. 拦截 ServeHTTP 调用,并执行任意代码。

  2. 在持续的链上对请求 / 响应流做变更。

  3. 中断中间件链条,或继续下一个中间件拦截器,最终到真正的请求处理程序上面。

这些听起来跟 express.js 中间件 很相似。我们研究了 许多资料 ,发现了一些 已经存在的解决方案 ,这些方案跟我们想要的非常吻合,但他们要么有 不必要的额外功能 ,要么需求 不对我们的胃口 。很明显,我们可以基于 express.js 编写中间件,安装这个干净整洁的组件之后,20 行以下代码就可以实现一个轻量级的 API

抽象

设计抽象时,首先我们要考虑的就是,如何写中间件函数(从现在起,可以称它为拦截器)。

答案很明显:

它们看起来就像 http.HandlerFunc ,带一个额外的参数 next,程序进行下一步的处理。这使得任何人都可以像编写简单函数一样,类似 http.HandlerFunc 这样来编写拦截器,做他们想做的,并能按照他们的意愿传递控制权。

接下来我们要考虑的就是,如何将拦截器挂到 http.Handlerhttp.HandlerFunc 上。想要达成这个目标,首先要做的就是定义 MiddlewareHandlerFunc ,简单来说就是 http.HandlerFunc 的一种类型(例如, type MiddlewareHandlerFunc http.HandlerFunc )。这将让我们在 http.HandlerFunc 的基础之上,构建一个更好的 API。现在给出一个 http.HandlerFunc ,我们想要的链式 API 大概是这样:

复制代码

func HomeRouter(w http.ResponseWriter, r *http.Request) {
		// Handle your request
}

// ...
// Some where when installing Hanlder
chain := MiddlewareHandlerFunc(HomeRouter).
  Intercept(NewElapsedTimeInterceptor()).
  Intercept(NewRequestIdInterceptor())

// Install it like regular HttpHandler
mux.Path("/home").HandlerFunc(http.HandlerFunc(chain))

http.HandlerFunc 转换成 MiddlewareHandlerFunc ,并通过调用 Intercept 方法来安装拦截器。 Intercept 的返回类型又是一个 MiddlewareHandlerFunc ,允许我们再次调用 Intercept

如果使用 Intercept 架构,非常值得注意的一点就是执行的先后顺序。因为调用 chain(responseWriter, request) 实际上就是间接调用上一个拦截器,并将其停止,即从拦截器的尾部回到处理程序的首部。这非常有意义,因为正在拦截调用;所以你应该在父程序前执行拦截。

简化

虽然逆向链式系统让抽象变得更清晰,但大多时候都会有一个预编译拦截数组,可以在不同的处理程序中被复用。另外,当我们把中间件定义为数组时,更倾向按执行顺序去声明这些数组,而不是终止的顺序。我们把这个数组拦截器称作: MiddlewareChain 。我们想要的中间件链大概是这样:

注意,这些中间件将按照链中出现的顺序调用,即 RequestIDInterceptorElapsedTimeInterceptor 。这增加了代码的重用性和可读性。

实现

一旦设计好了抽象内容,实现起来就会很顺利:

复制代码

/*
Copyright (c) 2019 DoorDash
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
package middleware

import "net/http"

// MiddlewareInterceptor intercepts an HTTP handler invocation, it is passed both response writer and request
// which after interception can be passed onto the handler function.
type MiddlewareInterceptor func(http.ResponseWriter, *http.Request, http.HandlerFunc)

// MiddlewareHandlerFunc builds on top of http.HandlerFunc, and exposes API to intercept with MiddlewareInterceptor.
// This allows building complex long chains without complicated struct manipulation
type MiddlewareHandlerFunc http.HandlerFunc


// Intercept returns back a continuation that will call install middleware to intercept
// the continuation call.
func (cont MiddlewareHandlerFunc) Intercept(mw MiddlewareInterceptor) MiddlewareHandlerFunc {
	return func(writer http.ResponseWriter, request *http.Request) {
		mw(writer, request, http.HandlerFunc(cont))
	}
}

// MiddlewareChain is a collection of interceptors that will be invoked in there index order
type MiddlewareChain []MiddlewareInterceptor

// Handler allows hooking multiple middleware in single call.
func (chain MiddlewareChain) Handler(handler http.HandlerFunc) http.Handler {
	curr := MiddlewareHandlerFunc(handler)
	for i := len(chain) - 1; i >= 0; i-- {
		mw := chain[i]
		curr = curr.Intercept(mw)
	}

	return http.HandlerFunc(curr)
}

这样一来,20 行(不包括注释)的代码,就能构建一个很不错的中间件库。在裸机上,这几行抽象代码连贯性也是令人惊叹的。这能让我们有条不紊地编写出流畅的 Http 中间件链。希望这几行 Go 语言代码也能给你带来好的中间件体验。

原文链接:

https://doordash.engineering/2019/07/22/writing-delightful-http-middlewares-in-go

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章