# 什么是中间件?
中间件是一种计算机软件,可为操作系统提供的软件应用程序提供服务,以便于各个软件之间的沟通,特别是系统软件和应用软件。广泛用于web应用和面向服务的体系结构等。
个人理解的“web的中间件” 就是 ”在原始请求上增加一些和原始请求数据无关的操作,可能在请求前也可能在请求后;比如:增加日志、验证 Token等“。
因为大部分中间件跟业务逻辑没什么关系,因此要求中间件需要具备:可插拔特性。不会影响业务逻辑,哪里要用插到哪里的。
# golang中间件原理
中间件处理程序是简单的http.Handler,它包装另一个http.Handler做请求的一些预处理和/或后处理。它被称为“中间件”,因为它位于Go Web服务器和实际处理程序之间的中间位置。
![图片出处:https://www.cnblogs.com/maji233/p/11237349.html](https://static.golangjob.cn/220509/dfcab26d5325e11d602abf0ddac8bc62.png)
mux 介绍:(https://www.jianshu.com/p/8ade70e51210)
中间件本身只是将其http.HandlerFunc作为其参数之一,包装它并返回一个新http.HandlerFunc的服务器来调用。
### 举个栗子便于理解:
```go
package main
import (
"fmt"
"log"
"net/http"
)
func foo(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "foo")
}
func bar(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "bar")
}
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println(r.URL.Path)
next.ServeHTTP(w, r)
})
}
func main() {
http.Handle("/foo", loggingMiddleware(http.HandlerFunc(foo)))
http.Handle("/bar", loggingMiddleware(http.HandlerFunc(bar)))
http.ListenAndServe(":8080", nil)
}
```
# golang 如何自定义中间件
#### 编写中间件的逻辑
- 自定义的中间件函数接收一个 http.Handler 作为参数;目的是可使用其他中间件或应用处理器,调用其他中间件或应用的方法就是调用其 HttpServe 的方法。
- 将 http.Handler 作为返回值,这样其他中间件可以使用 http.Handler 作为输入的参数,达到链式调用。
例如:
``` go
func middlewareHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 在执行应用处理器前编译中间件的逻辑
next.ServeHTTP(w, r)
// 在执行应用处理器后将会执行的编译中间件的逻辑
})
}
```
通过向下传递 http.Handler 实现了多个中间件的调用。
```go
package main
import (
"fmt"
"log"
"net/http"
"time"
)
func logginHandler(next http.Handler) http.Handler{
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request){
start := time.Now()
log.Printf("Started %s %s",r.Method, r.URL.Path)
next.ServeHTTP(w,r)
log.Printf("Completed %s in %v", r.URL.Path, time.Since(start))
})
}
func index(w http.ResponseWriter,r *http.Request ){
log.Println("Executing index handler")
fmt.Fprintf(w,"welcome!")
}
func about(w http.ResponseWriter, r *http.Request){
log.Println("Executing about handler")
fmt.Fprintf(w,"Go Middleware")
}
func iconHandler(w http.ResponseWriter, r *http.Request){
}
func main() {
http.HandleFunc("/favicon.ico", iconHandler)
indexHandler := http.HandlerFunc(index)
aboutHandler := http.HandlerFunc(about)
http.Handle("/",logginHandler(indexHandler))
http.Handle("/about",logginHandler(aboutHandler))
server := &http.Server{
Addr:":8080",
}
log.Println("Listening...")
server.ListenAndServe()
}
```
- 上面的代码关键是 logginHandler 这个日志处理器,作为中间件会拦截请求,进行处理输出日志,通过next.ServeHTTP(w,r)将请求向下进行传递
- 创建两个简单路由处理器index和about。都是向客户端输出不同的文本。
- http.Handle("/",logginHandler(indexHandler)) 通过 logginHandler 函数进行包裹,日志中间件可以拦截到发起的请求进行一些中间件逻辑,中间件逻辑可以位于next.ServeHTTP(w,r)之前或之后。通过下面的输出我们可以清晰可见代码执行的顺序。
```
Listening...
Started GET /
Executing index handler
Completed / in 41.129µs
Started GET /
Executing index handler
Completed / in 50.475µs
Started GET /about
Executing about handler
Completed /about in 49.483µs
```
# 更加优雅的中间件写法:
先看一个美观的栗子:
```go
r = NewRouter()
r.Use(logger)
r.Use(timeout)
r.Use(ratelimit)
r.Add("/",helloHandler)
```
通过多步设置,我们拥有了和上一节差不多的执行函数链。胜在直观易懂,如果我们要增加或者删除中间件,只要简单地增加删除对应的Use()调用就可以了,非常方便。
从框架的角度来讲,怎么实现这样的功能呢?也不复杂:
```go
type middleware func(http.Handler) http.Handler
type Router struct {
middlewareChain []middleware
mux map[string]http.Handler
}
func NewRouter() *Router {
return &Router{}
}
func (r *Router) Use(m middleware) {
r.middlewareChain = append(r.middlewareChain, m)
}
func (r *Router) Add(route string, h http.Handler) {
var mergedHandler = h
for i := len(r.middlewareChain) - 1; i >= 0; i-- {
mergedHandler = r.middlewareChain[i](mergedHandler)
}
r.mux[route] = mergedHandler
}
```
# 哪些事情适合在中间件做?
以go的chi 框架参考:
```
compress.go
=>对http的响应体进行压缩处理
heartbeat.go
=>设置一个特殊的路由,例如/ping; /healthcheck,用来给负载均衡一类的前置服务进行探活
logger.go
=>打印请求处理处理日志,例如请求处理时间,请求路由
profiler.go
=>挂载pprof需要的路由,如`/pprof`、`/pprof/trace`到系统中
realip.go
=>从请求头中读取X-Forwarded-For和X-Real-IP,将http.Request中的RemoteAddr修改为得到的RealIP
requestid.go
=>为本次请求生成单独的requestid,可一路透传,用来生成分布式调用链路,也可用于在日志中串连单次请求的所有逻辑
timeout.go
=>用context.Timeout设置超时时间,并将其通过http.Request一路透传下去
throttler.go
=>通过定长大小的channel存储token,并通过这些token对接口进行限流
```
[参考资料](https://www.jianshu.com/p/8ade70e51210)
[参考资料](https://segmentfault.com/a/1190000018819804)
[参考资料](https://www.jianshu.com/p/877492d3bcc8)
有疑问加站长微信联系(非本文作者))