中间件是后台工程中最重要的概念之一。
它们是独立的,可重用的软件组件,将不同的系统连接起来。
在 web 开发中, client
与 server
之间普通存在着一个或者多个中间件。潜在地为数据与 UI 搭建了一座桥梁。
中间件独立于 Http 的请求与响应。
一个中间件的输出可以作为另外一个中间件的输入。
通过这种方式组成了一条中间件链。
为什么要中间件?
在 web 应用中,中间件的应用可以让我们集中和重复每个请求与响应的的通用功能。比如最常见的为每个请求进行日志记录。
在设计中间件这一方面,有一些明确的规则与模式需要我们去遵守。
这篇文章将会通过一个简单的应用来演示这些。
package main
import (
"log"
"math/rand"
"net/http"
"time"
)
func main() {
// Initialize a new ServeMux
mux := http.NewServeMux()
// Register home function as handler for /home
mux.HandleFunc("/home", home)
// Run HTTP server with custom ServeMux at localhost:4000
err := http.ListenAndServe(":8080", mux)
// Exit if any errors occur
log.Fatal(err)
}
// Handles HTTP requests to /home
func home(w http.ResponseWriter, r *http.Request) {
// Writes a byte slice with the text "Welcome to Go Middleware"
// in the response body
time.Sleep(time.Second*time.Duration(rand.Intn(5)) + 1)
w.Write([]byte("Welcome to Go Middleware"))
}
在上述代码中,我们创建了一个新的并且非默认的 ServeMux
, 添加了路由规则 /home
映射到处理函数 home
,
随后将这个 ServeMux
注册监听到 8080
端口。
将代码运行起来并在终端进行请求会得到如下:
➜ go run . &
[1] 28901
➜ curl localhost:8080/home
Welcome to Go Middleware
在我们开始创建中间件前,理论一些理论知识是有益的。
特别是,在 Go 中的 handler
是什么。
在 Go 中,handler
是满足 http.Handler
接口定义的一个对象或者结构体。
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
通过 ListenAndServe
方法签名
func ListenAndServe(addr string, handler Handler) error {
... ...
}
我们知道 ServeMux
本身就是一个 http.Handler
.
当它接收到一个 http 请求时,会根据 URL 找到匹配的处理器,比如 home
, home
方法的定义同样符合 http.Handler
的接口声明,通过 http.HandlerFunc(...)
可以将其转化为 handler
, 所以路由处理同样也是 http.Handler
.
在 URL 匹配时,ServeMux
会调用路由处理器的 ServeHTTP
方法并将请求传递给它, 处理器在随后进行它的逻辑处理并响应。
总之,你可以认为 Go 的 web 应用路由是通过一系列的 http.Handler
与方法 ServerHTTP
依次调用来实现的。
因此,为了适应进入这一条链中,Go 中间件同样也需要扮演 http.Handler
的角色。
它执行一些特定的逻辑与调用下一个 http.Handler
的 ServeHTTP
的方法。
举例子
我们需要一个 traceIdHandlerProxy
的中间件,顾名思义就是在请求与响应头添加 traceId
来做分布式链接追踪的基础工作.
第一种方式使用构造器
// trace id 代理 http.Handler
type traceIdMiddle struct {
next http.Handler
}
// satisfy http.Handler interface
func (h *traceIdMiddle) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
// traceIdHandlerProxy logic
traceId := time.Now().Format("20060102150405")
r.Header.Set("trace_id", traceId)
rw.Header().Set("trace_id", traceId)
// forward to next http.Handler
h.next.ServeHTTP(rw, r)
}
创建出一个新的结构体 traceIdHandlerProxy
,添加方法 ServeHTTP(ResponseWriter, *Request)
来满足 http.Handler
接口的要求。
这样就成为了一个 http.Handler
.
优点:
- 简单直观,面向对象(结构体)的开发者很熟悉 缺点:
- 需要创建大量的中间件结构体
func traceIdMiddle(next http.Handler) http.Handler {
f := func(rw http.ResponseWriter, r *http.Request) {
// traceIdHandlerProxy logic
traceId := time.Now().Format("20060102150405")
r.Header.Set("trace_id", traceId)
rw.Header().Set("trace_id", traceId)
// forward to next http.Handler
next.ServeHTTP(rw, r)
}
return http.HandlerFunc(f)
}
优点:
- 代码量少,灵活,使用高阶函数符合函数式编程规范 缺点:
- 无
接下来的示例都会以高阶函数的方式来完成例子
上面的代码片段包含很多信息,一起来看看:
- traceIdMiddle 是一个接收参数为
http.Handler
,返回参数也为http.Handler
的函数。 - 中间件内部创建了一个符合
handler
签名的函数f
, 它接收两个参数,分别是http.Responsewriter
与*http.Request
,中间件的逻辑也包含在函数f
内。 - 函数
f
内部通过调用下一个http.Handler
的ServerHTTP
来传递 http 请求。 - 函数
f
通过http.HandlerFunc
函数转换与适配为http.Handler
并返回。 f
形成了一个闭包,因为在其被返回后依然能够访问到变量next
。
主要内容是 Go 中间件是一个函数,它接受请求链中的下一个 http.Handler
作为参数。 它返回一个 http.Handler
,该处理程序在执行链中的下一个处理程序之前或者之后执行一些逻辑。
现在我们了解中间件正确的概念与理论,接下来的事情会变得更加简单。
在这一节,我们尝试创建多个不同的中间件,一个是记录 http 的请求日志,一个是添加添加基础的安全 header 信息,另外一个是统计业务处理的时间。
package main
import (
"fmt"
"net/http"
"time"
)
func logRequestMiddleware(handler http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
fmt.Printf("LOG %s - %s %s %s\n", r.RemoteAddr, r.Proto, r.Method, r.URL)
// continue handle
handler.ServeHTTP(rw, r)
})
}
func secureHeadersMiddleware(handler http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.Header().Set("X-XSS-Protection", "1; mode-block")
rw.Header().Set("X-Frame-Options", "deny")
// continue handle
handler.ServeHTTP(rw, r)
})
}
func handleTimeLogMiddle(handler http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
now := time.Now()
// continue handle
handler.ServeHTTP(rw, r)
duration := time.Since(now).Milliseconds()
fmt.Printf("请求处理时间: %d(ms)", duration)
})
}
logRequestMiddleware
记录了网络地址,协议版本,与请求方法,请求 URL 到标准输出中。这些信息在 http.Request
是可获取的。
secureHeadersMiddleware
在响应中设置了两个安全头部信息来防御对抗 XSS 等攻击。
handleTimeLogMiddle
在下一个 handler
执行前记录了开始时间,在处理完成后记录结束时间,以此来计算总共花费处理时长,这可以很方便找出处理时长较高的逻辑。
但是,在我们注册这些中间件之前,很重要的一点是要明确知道中间件位置的不同可能对我们应用行为造成的影响。
特别是,如果我们想要一个中间作用于每个 HTTP
请求,那么我们应该将它放置在 ServeMux
之前,换种方式说,我们需要将 ServeMux
作为下一个待处理 http.Handler
传递给我们的中间件。
另一方面,如果我们想要中间件作用于特定的路由,我们需要将它放置在 ServeMux
之后,将其作为参数传递给我们的中间件来实现这一点。
在这个例子中,我们需要将 logHttpRequestMiddle
与 securityHeaderMiddle
中间件作用于每一个 HTTP 请求。因此两个中间件都应该放置在 ServeMux
之前。
package main
import (
"log"
"math/rand"
"net/http"
"time"
)
func main() {
// Initialize a new ServeMux
mux := http.NewServeMux()
// Register home function as handler for /home
mux.HandleFunc("/home", home)
// Run HTTP server with custom ServeMux at localhost:4000
err := http.ListenAndServe(":8080", logHttpRequestMiddle(securityHeaderMiddle(mux)))
// Exit if any errors occur
log.Fatal(err)
}
// Handles HTTP requests to /home
func home(w http.ResponseWriter, r *http.Request) {
// Writes a byte slice with the text "Welcome to Go Middleware"
// in the response body
time.Sleep(time.Second*time.Duration(rand.Intn(5)) + 1)
w.Write([]byte("Welcome to Go Middleware"))
}
另外我们只需要统计 home
处理器的执行时长,因此我们需要 handleTimeLogMiddle
中间件放置在 ServeMux
之后。
package main
import (
"log"
"math/rand"
"net/http"
"time"
)
func main() {
// Initialize a new ServeMux
mux := http.NewServeMux()
// Register home function as handler for /home
mux.HandleFunc("/home", func(rw http.ResponseWriter, r *http.Request) {
handleTimeLogMiddle(http.HandlerFunc(home)).ServeHTTP(rw, r)
})
// Run HTTP server with custom ServeMux at localhost:4000
err := http.ListenAndServe(":8080", logHttpRequestMiddle(securityHeaderMiddle(mux)))
// Exit if any errors occur
log.Fatal(err)
}
// Handles HTTP requests to /home
func home(w http.ResponseWriter, r *http.Request) {
// Writes a byte slice with the text "Welcome to Go Middleware"
// in the response body
time.Sleep(time.Second*time.Duration(rand.Intn(5)) + 1)
w.Write([]byte("Welcome to Go Middleware"))
}
通过 curl
命令来检查中间件工作情况。
- 启动 HTTP 服务器并以后台方式运行
- curl 命令请求
/home
路由地址-X
选项用来指定请求的方式为GET
-i
选项用来打印响应header
信息
➜ go-middle go run . &
[1] 30145
➜ go-middle curl -X GET -i localhost:8080/home
LOG [::1]:53846 - HTTP/1.1 GET /home
请求处理时间: 1004(ms)HTTP/1.1 200 OK
X-Frame-Options: deny
X-Xss-Protection: 1; mode-block
Date: Mon, 10 Jan 2022 15:11:00 GMT
Content-Length: 24
Content-Type: text/plain; charset=utf-8
Welcome to Go Middleware
我们可以看到 logHttpRequestMiddle
中间件打印了请求日志
LOG [::1]:53846 - HTTP/1.1 GET /home
handleTimeLogMiddle
打印出了执行时长
请求处理时间: 1004(ms)HTTP/1.1 200 OK
securityHeaderMiddle
设置了头部信息
X-Frame-Options: deny
X-Xss-Protection: 1; mode-block
Date: Mon, 10 Jan 2022 15:11:00 GMT
Content-Length: 24
Content-Type: text/plain; charset=utf-8
走到这里意味着你对 Go 中间件编写已经有了初步的了解。
你也可以发现通过这种方式,当中间件链数量太多,代码并不具备很好的拓展性,从而难以维护。
幸运的是,已经有第三方包来帮助我们管理中间件。其中有一些比较出名,可以去了解一下。