中间件(通常)是一小段代码,它们接收一个请求,对其进行处理,每个中间件只处理一件事情,完成后将其传递给另一个中间件或最终处理程序,这样就做到了程序的解耦。如果没有中间件那么我们必须在最终的处理程序中来完成这些处理操作,这无疑会造成处理程序的臃肿和代码复用率不高的问题。中间件的一些常见用例是请求日志记录, Header
操纵、 HTTP
请求认证和 ResponseWriter
劫持等等。
画外音:上面这段描述中间件的文字,跟我很早前在Laravel源码解析之中间件写的几乎一样(其实这图也是从那里拿过来的)。再次说明做开发时间长了以后掌握一些编程的思想有时候比掌握一门编程语言更重要,这不咱们就又用Go来写中间件了。
创建中间件
接下来我们用 Go
创建中间件,中间件只将 http.HandlerFunc
作为其参数,在中间件里将其包装并返回新的 http.HandlerFunc
供服务器服务复用器调用。这里我们创建一个新的类型 Middleware
,这会让最后一起链式调用多个中间件变的更简单。
type Middleware func(http.HandlerFunc) http.HandlerFunc
下面的中间件通用代码模板让我们平时编写中间件变得更容易。
中间件代码模板
中间件是使用装饰器模式实现的,下面的中间件通用代码模板让我们平时编写中间件变得更容易,我们在自己写中间件的时候只需要往样板里填充需要的代码逻辑即可。
func createNewMiddleware() Middleware {
// 创建一个新的中间件
middleware := func(next http.HandlerFunc) http.HandlerFunc {
// 创建一个新的handler包裹next
handler := func(w http.ResponseWriter, r *http.Request) {
// 中间件的处理逻辑
......
// 调用下一个中间件或者最终的handler处理程序
next(w, r)
}
// 返回新建的包装handler
return handler
}
// 返回新建的中间件
return middleware
}
使用中间件
我们创建两个中间件,一个用于记录程序执行的时长,另外一个用于验证请求用的是否是指定的 HTTPMethod
,创建完后再用定义的 Chain
函数把 http.HandlerFunc
和应用在其上的中间件链起来,中间件会按添加顺序依次执行,最后执行到处理函数。完整的代码如下:
package main
import (
"fmt"
"log"
"net/http"
"time"
)
type Middleware func(http.HandlerFunc) http.HandlerFunc
// 记录每个URL请求的执行时长
func Logging() Middleware {
// 创建中间件
return func(f http.HandlerFunc) http.HandlerFunc {
// 创建一个新的handler包装http.HandlerFunc
return func(w http.ResponseWriter, r *http.Request) {
// 中间件的处理逻辑
start := time.Now()
defer func() { log.Println(r.URL.Path, time.Since(start)) }()
// 调用下一个中间件或者最终的handler处理程序
f(w, r)
}
}
}
// 验证请求用的是否是指定的HTTP Method,不是则返回 400 Bad Request
func Method(m string) Middleware {
return func(f http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != m {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
f(w, r)
}
}
}
// 把应用到http.HandlerFunc处理器的中间件
// 按照先后顺序和处理器本身链起来供http.HandleFunc调用
func Chain(f http.HandlerFunc, middlewares ...Middleware) http.HandlerFunc {
for _, m := range middlewares {
f = m(f)
}
return f
}
// 最终的处理请求的http.HandlerFunc
func Hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "hello world")
}
func main() {
http.HandleFunc("/", Chain(Hello, Method("GET"), Logging()))
http.ListenAndServe(":8080", nil)
}
运行程序后会打开浏览器访问 http://localhost:8080
会有如下输出:
2020/02/07 21:07:52 / 359.503µs
2020/02/07 21:09:17 / 34.727µs
到这里怎么用 Go
编写和使用中间件就讲完,也就十分钟吧。不过这里更多的是探究实现原理,那么在生产环境怎么自己使用编写的这些中间件呢,我们接着往下看。
使用 gorilla/mux
应用中间件
上面我们探讨了如何创建中间件,但是使用上每次用 Chain
函数链接多个中间件和处理程序还是有些不方便,而且在上一篇文章中我们已经开始使用 gorilla/mux
提供的 Router
作为路由器了。好在 gorrila.mux
支持向路由器添加中间件,如果发现匹配项,则按照添加中间件的顺序执行中间件,包括其子路由器也支持添加中间件。
gorrila.mux
路由器使用 Use
方法为路由器添加中间件, Use
方法的定义如下:
func (r *Router) Use(mwf ...MiddlewareFunc) {
for _, fn := range mwf {
r.middlewares = append(r.middlewares, fn)
}
}
它可以接受多个 mux.MiddlewareFunc
类型的参数, mux.MiddlewareFunc
的类型声明为:
type MiddlewareFunc func(http.Handler) http.Handler
跟我们上面定义的 Middleware
类型很像也是一个函数类型,不过函数的参数和返回值都是 http.Handler
接口,在《深入学习用 Go 编写 HTTP 服务器》中我们详细讲过 http.Handler
它 是 net/http
中定义的接口用来表示处理 HTTP 请求的对象,其对象必须实现 ServeHTTP
方法。我们把上面说的中间件模板稍微更改下就能创建符合 gorrila.mux
要求的中间件:
func CreateMuxMiddleware() mux.MiddlewareFunc {
// 创建中间件
return func(f http.Handler) http.Handler {
// 创建一个新的handler包装http.HandlerFunc
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 中间件的处理逻辑
......
// 调用下一个中间件或者最终的handler处理程序
f.ServeHTTP(w, r)
})
}
}
接下来,我们把上面自定义的两个中间件进行改造,然后应用到我们一直在使用的 http_demo
项目上,为了便于管理在项目中新建 middleware
目录,两个中间件分别放在 log.go
和 http_method.go
中
//middleware/log.go
func Logging() mux.MiddlewareFunc {
// 创建中间件
return func(f http.Handler) http.Handler {
// 创建一个新的handler包装http.HandlerFunc
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 中间件的处理逻辑
start := time.Now()
defer func() { log.Println(r.URL.Path, time.Since(start)) }()
// 调用下一个中间件或者最终的handler处理程序
f.ServeHTTP(w, r)
})
}
}
// middleware/http_demo.go
func Method(m string) mux.MiddlewareFunc {
return func(f http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != m {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
f.ServeHTTP(w, r)
})
}
}
然后在我们的路由器中进行引用:
func RegisterRoutes(r *mux.Router) {
r.Use(middleware.Logging())// 全局应用
indexRouter := r.PathPrefix("/index").Subrouter()
indexRouter.Handle("/", &handler.HelloHandler{})
userRouter := r.PathPrefix("/user").Subrouter()
userRouter.HandleFunc("/names/{name}/countries/{country}", handler.ShowVisitorInfo)
userRouter.Use(middleware.Method("GET"))//给子路由器应用
}
再次编译启动运行程序后访问
http://localhost:8080/user/names/James/countries/NewZealand
从控制台里可以看到,记录了这个请求的处理时长:
2020/02/08 09:29:50 Starting HTTP server...
2020/02/08 09:55:20 /user/names/James/countries/NewZealan 51.157µs
到这里我们探究完了编写Web中间件的过程和原理,在实际开发中只需要根据自己的需求按照我们给的中间件代码模板编写中间件即可,在编写中间件的时候也要注意他们的职责范围,不要所有逻辑都往里放。
资料下载
点击下方卡片关注公众号,发送特定关键字获取对应精品资料!
回复「电子书」,获取入门、进阶 Go 语言必看书籍。
回复「视频」,获取价值 5000 大洋的视频资料,内含实战项目(不外传)!
回复「路线」,获取最新版 Go 知识图谱及学习、成长路线图。
回复「面试题」,获取四哥精编的 Go 语言面试题,含解析。
回复「后台」,获取后台开发必看 10 本书籍。
对了,看完文章,记得点击下方的卡片。关注我哦~ 👇👇👇
如果您的朋友也在学习 Go 语言,相信这篇文章对 TA 有帮助,欢迎转发分享给 TA,非常感谢!