英文原文链接: http://www.alexedwards.net/blog/making-and-using-middleware
当你正在构建一个Web应用程序有可能要运行许多(甚至全部)的HTTP请求一些共享功能,你可能想记录每一个request,gzip压缩的每个response,或者做一些繁重的处理或者缓存检查。
实现这个共享功能的一种方法是将其设置为中间件,他可以作为一个独立的程序,在正常的handlers处理之前。根本不需要重写代码:如果你想用一个中间件,就把它加上应用中;如果你改变主意了,去掉就好了。就这么简单。
ServeMux => Middleware Handler => Application Handler
ServeMux => MiddlewareHandler => ApplicationHandler
这篇文章,我会给大家介绍怎么自己去实现一个自定义的middleware模式。以及通过使用第三方的中间件软件包的一些具体的实例。
基本原则:
在Go语言中实现和使用middleware是非常简单的。
我来解释如何实现:
首先你要知道go 的http handle,这里假设你是知道的
func messageHandler(message string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(message) }) }
funcmessageHandler(messagestring) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(message) }) }
这上面这个代码片段里面我们的逻辑很简单只是一个简单的 w.Write() 然后我们使用 http.HandlerFunc 适配器来转化这个闭包,并返回。
我们可以使用一个相同的方法来创建一个 handler 链。可以使用 handler 代替参数 string 传进闭包,然后把控制 handler 给传进来的 handler,并且调用 ServeHTTP() 方法。
这给了我们一个完整的模式构建中间件:
func exampleMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Our middleware logic goes here... next.ServeHTTP(w, r) }) }
funcexampleMiddleware(nexthttp.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Our middleware logic goes here... next.ServeHTTP(w, r) }) }
你注意到这个中间件有一个这样的函数结构 func(http.Handler) http.Handler 。它接受一个 handler 作为参数,并且返回一个 handler。这里有两个很有用的原因:
比如:
http.Handle("/", middlewareOne(middlewareTwo(finalHandler)))
http.Handle("/", middlewareOne(middlewareTwo(finalHandler)))
控制流说明:
让我们来看一个带有多个中间件的例子,并且把日志输出到控制台:
package main import ( "log" "net/http" ) func middlewareOne(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.Println("Executing middlewareOne") next.ServeHTTP(w, r) log.Println("Executing middlewareOne again") }) } func middlewareTwo(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.Println("Executing middlewareTwo") if r.URL.Path != "/" { return } next.ServeHTTP(w, r) log.Println("Executing middlewareTwo again") }) } func final(w http.ResponseWriter, r *http.Request) { log.Println("Executing finalHandler") w.Write([]byte("OK")) } func main() { finalHandler := http.HandlerFunc(final) http.Handle("/", middlewareOne(middlewareTwo(finalHandler))) http.ListenAndServe(":3000", nil) }
package main import ( "log" "net/http" ) funcmiddlewareOne(nexthttp.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.Println("Executing middlewareOne") next.ServeHTTP(w, r) log.Println("Executing middlewareOne again") }) } funcmiddlewareTwo(nexthttp.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.Println("Executing middlewareTwo") if r.URL.Path != "/" { return } next.ServeHTTP(w, r) log.Println("Executing middlewareTwo again") }) } funcfinal(w http.ResponseWriter, r *http.Request) { log.Println("Executing finalHandler") w.Write([]byte("OK")) } funcmain() { finalHandler := http.HandlerFunc(final) http.Handle("/", middlewareOne(middlewareTwo(finalHandler))) http.ListenAndServe(":3000", nil) }
然后我们执行 go run main.go 在浏览器打开http://localhost:3000。 你会看到下面的输出。
我们能够很清楚的看到handle的流程控制。我们嵌套他们的返回顺序。我们可以通过中间件中得 return 随时停止handle链的控制。
在上面的代码中我们在 middlewareTwo
function包含了retrun 语句。我们在浏览器中打开http://localhost:3000/foo,我们会看到。
2015/12/19 04:21:57 Executing middlewareOne 2015/12/19 04:21:57 Executing middlewareTwo 2015/12/19 04:21:57 Executing middlewareOne again 2015/12/19 04:21:57 Executing middlewareOne 2015/12/19 04:21:57 Executing middlewareTwo 2015/12/19 04:21:57 Executing middlewareOne again
2015/12/19 04:21:57 ExecutingmiddlewareOne 2015/12/19 04:21:57 ExecutingmiddlewareTwo 2015/12/19 04:21:57 ExecutingmiddlewareOneagain 2015/12/19 04:21:57 ExecutingmiddlewareOne 2015/12/19 04:21:57 ExecutingmiddlewareTwo 2015/12/19 04:21:57 ExecutingmiddlewareOneagain
我们实现一个真实的项目的示例:
我们实现一个判断请求是不是XMl的功能,我们要实现一个中间件。用来检查的请求体的存在。检查请求体,以确保它是XML。如果其中检查失败,我希望我们的中间件输出错误信息然后终止我们的handle处理。
package main import ( "bytes" "net/http" ) func enforceXMLHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Check for a request body if r.ContentLength == 0 { http.Error(w, http.StatusText(400), 400) return } // Check its MIME type buf := new(bytes.Buffer) buf.ReadFrom(r.Body) if http.DetectContentType(buf.Bytes()) != "text/xml; charset=utf-8" { http.Error(w, http.StatusText(415), 415) return } next.ServeHTTP(w, r) }) } func main() { finalHandler := http.HandlerFunc(final) http.Handle("/", enforceXMLHandler(finalHandler)) http.ListenAndServe(":3000", nil) } func final(w http.ResponseWriter, r *http.Request) { w.Write([]byte("OK")) }
package main import ( "bytes" "net/http" ) funcenforceXMLHandler(nexthttp.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Check for a request body if r.ContentLength == 0 { http.Error(w, http.StatusText(400), 400) return } // Check its MIME type buf := new(bytes.Buffer) buf.ReadFrom(r.Body) if http.DetectContentType(buf.Bytes()) != "text/xml; charset=utf-8" { http.Error(w, http.StatusText(415), 415) return } next.ServeHTTP(w, r) }) } funcmain() { finalHandler := http.HandlerFunc(final) http.Handle("/", enforceXMLHandler(finalHandler)) http.ListenAndServe(":3000", nil) } funcfinal(w http.ResponseWriter, r *http.Request) { w.Write([]byte("OK")) }
为了检验我们的中间件是否实现了这个功能,我们首先创建一个XML文件。
$ cat > books.xml <?xml version="1.0"?> <books> <book> <author>H. G. Wells</author> <title>The Time Machine</title> <price>8.50</price> </book> </books>
$ cat > books.xml <?xmlversion="1.0"?> <books> <book> <author>H. G. Wells</author> <title>TheTimeMachine</title> <price>8.50</price> </book> </books>
然后通过使用cURL来进行模拟请求:
$ curl -i localhost:3000 HTTP/1.1 400 Bad Request Content-Type: text/plain; charset=utf-8 Content-Length: 12 Bad Request $ curl -i -d "This is not XML" localhost:3000 HTTP/1.1 415 Unsupported Media Type Content-Type: text/plain; charset=utf-8 Content-Length: 23 Unsupported Media Type $ curl -i -d @books.xml localhost:3000 HTTP/1.1 200 OK Date: Fri, 17 Oct 2014 13:42:10 GMT Content-Length: 2 Content-Type: text/plain; charset=utf-8 OK
$ curl -i localhost:3000 HTTP/1.1 400 BadRequest Content-Type: text/plain; charset=utf-8 Content-Length: 12 BadRequest $ curl -i -d "This is not XML" localhost:3000 HTTP/1.1 415 UnsupportedMediaType Content-Type: text/plain; charset=utf-8 Content-Length: 23 UnsupportedMediaType $ curl -i -d @books.xmllocalhost:3000 HTTP/1.1 200 OK Date: Fri, 17 Oct 2014 13:42:10 GMT Content-Length: 2 Content-Type: text/plain; charset=utf-8 OK
接下来给大家介绍一下第三方中间件的使用:
秉承不造轮子的原则,其实在Github上有很多实现了一些功能的中间件。比如这里给大家介绍2个基础验证的中间件 goji/httpauth 和Gorilla’s LoggingHandler
首先我们需要引入第三方包
$ go get github.com/goji/httpauth
$ gogetgithub.com/goji/httpauth
package main import ( "github.com/goji/httpauth" "net/http" ) func main() { finalHandler := http.HandlerFunc(final) authHandler := httpauth.SimpleBasicAuth("username", "password") http.Handle("/", authHandler(finalHandler)) http.ListenAndServe(":3000", nil) } func final(w http.ResponseWriter, r *http.Request) { w.Write([]byte("OK")) }
package main import ( "github.com/goji/httpauth" "net/http" ) funcmain() { finalHandler := http.HandlerFunc(final) authHandler := httpauth.SimpleBasicAuth("username", "password") http.Handle("/", authHandler(finalHandler)) http.ListenAndServe(":3000", nil) } funcfinal(w http.ResponseWriter, r *http.Request) { w.Write([]byte("OK")) }
如果你运行这个例子,你应该得到你所期望的有效和无效的凭证响应
$ curl -i username:password@localhost:3000 HTTP/1.1 200 OK Content-Length: 2 Content-Type: text/plain; charset=utf-8 OK $ curl -i username:wrongpassword@localhost:3000 HTTP/1.1 401 Unauthorized Content-Type: text/plain; charset=utf-8 Www-Authenticate: Basic realm=""Restricted"" Content-Length: 13 Unauthorized
$ curl -i username:password@localhost:3000 HTTP/1.1 200 OK Content-Length: 2 Content-Type: text/plain; charset=utf-8 OK $ curl -i username:wrongpassword@localhost:3000 HTTP/1.1 401 Unauthorized Content-Type: text/plain; charset=utf-8 Www-Authenticate: Basicrealm=""Restricted"" Content-Length: 13 Unauthorized
Gorilla’s LoggingHandler和 Apache-style logs 有一些区别
以下是我们在其中写入日志到server.log文件一个简单的例子:
首先还是引入第三包
go get github.com/gorilla/handlers
gogetgithub.com/gorilla/handlers
package main import ( "github.com/gorilla/handlers" "net/http" "os" ) func main() { finalHandler := http.HandlerFunc(final) logFile, err := os.OpenFile("server.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) if err != nil { panic(err) } http.Handle("/", handlers.LoggingHandler(logFile, finalHandler)) http.ListenAndServe(":3000", nil) } func final(w http.ResponseWriter, r *http.Request) { w.Write([]byte("OK")) }
package main import ( "github.com/gorilla/handlers" "net/http" "os" ) funcmain() { finalHandler := http.HandlerFunc(final) logFile, err := os.OpenFile("server.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) if err != nil { panic(err) } http.Handle("/", handlers.LoggingHandler(logFile, finalHandler)) http.ListenAndServe(":3000", nil) } funcfinal(w http.ResponseWriter, r *http.Request) { w.Write([]byte("OK")) }
在一个简单的情况下,这样我们的代码是相当清楚的。但是,如果我们想用LoggingHandler作为一个更大的中间件链中的一部分会发生什么?我们可以很容易地结束了一个声明,看起来像这样:
http.Handle("/", handlers.LoggingHandler(logFile, authHandler(enforceXMLHandler(finalHandler))))
http.Handle("/", handlers.LoggingHandler(logFile, authHandler(enforceXMLHandler(finalHandler))))
不过这看起来太糟糕了。
我们可以通过创建一个构造函数打来整理一下我们给它取名为(myLoggingHandler)
和signature func(http.Handler) http.Handler
.这样就会是我们的代码更加整洁和可读性:
func myLoggingHandler(h http.Handler) http.Handler { logFile, err := os.OpenFile("server.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) if err != nil { panic(err) } return handlers.LoggingHandler(logFile, h) } func main() { finalHandler := http.HandlerFunc(final) http.Handle("/", myLoggingHandler(finalHandler)) http.ListenAndServe(":3000", nil) }
funcmyLoggingHandler(h http.Handler) http.Handler { logFile, err := os.OpenFile("server.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) if err != nil { panic(err) } return handlers.LoggingHandler(logFile, h) } funcmain() { finalHandler := http.HandlerFunc(final) http.Handle("/", myLoggingHandler(finalHandler)) http.ListenAndServe(":3000", nil) }
$ cat server.log 127.0.0.1 - - [21/Oct/2014:18:56:43 +0100] "GET / HTTP/1.1" 200 2 127.0.0.1 - - [21/Oct/2014:18:56:36 +0100] "POST / HTTP/1.1" 200 2 127.0.0.1 - - [21/Oct/2014:18:56:43 +0100] "PUT / HTTP/1.1" 200 2
$ catserver.log 127.0.0.1 - - [21/Oct/2014:18:56:43 +0100] "GET / HTTP/1.1" 200 2 127.0.0.1 - - [21/Oct/2014:18:56:36 +0100] "POST / HTTP/1.1" 200 2 127.0.0.1 - - [21/Oct/2014:18:56:43 +0100] "PUT / HTTP/1.1" 200 2
这里还有一个比较完整结构的中间件使用的示例:
package main import ( "bytes" "github.com/goji/httpauth" "github.com/gorilla/handlers" "net/http" "os" ) func enforceXMLHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.ContentLength == 0 { http.Error(w, http.StatusText(400), 400) return } buf := new(bytes.Buffer) buf.ReadFrom(r.Body) if http.DetectContentType(buf.Bytes()) != "text/xml; charset=utf-8" { http.Error(w, http.StatusText(415), 415) return } next.ServeHTTP(w, r) }) } func myLoggingHandler(h http.Handler) http.Handler { logFile, err := os.OpenFile("server.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) if err != nil { panic(err) } return handlers.LoggingHandler(logFile, h) } func main() { indexHandler := http.HandlerFunc(index) authHandler := httpauth.SimpleBasicAuth("username", "password") http.Handle("/", myLoggingHandler(authHandler(enforceXMLHandler(indexHandler)))) http.ListenAndServe(":3000", nil) } func index(w http.ResponseWriter, r *http.Request) { w.Write([]byte("OK")) }
package main import ( "bytes" "github.com/goji/httpauth" "github.com/gorilla/handlers" "net/http" "os" ) funcenforceXMLHandler(nexthttp.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.ContentLength == 0 { http.Error(w, http.StatusText(400), 400) return } buf := new(bytes.Buffer) buf.ReadFrom(r.Body) if http.DetectContentType(buf.Bytes()) != "text/xml; charset=utf-8" { http.Error(w, http.StatusText(415), 415) return } next.ServeHTTP(w, r) }) } funcmyLoggingHandler(h http.Handler) http.Handler { logFile, err := os.OpenFile("server.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) if err != nil { panic(err) } return handlers.LoggingHandler(logFile, h) } funcmain() { indexHandler := http.HandlerFunc(index) authHandler := httpauth.SimpleBasicAuth("username", "password") http.Handle("/", myLoggingHandler(authHandler(enforceXMLHandler(indexHandler)))) http.ListenAndServe(":3000", nil) } funcindex(w http.ResponseWriter, r *http.Request) { w.Write([]byte("OK")) }
有很多人不太喜欢中间件的设计模式,不过我还是慢喜欢的。
Go语言的Http 中间件实现