轉載自 https://www.alexedwards.net/blog/making-and-using-middleware
當你構建一個web應用程序時,可能有一些共享的功能,你想參加許多(甚至是全部)HTTP請求。 您可能想要記錄每個請求,gzip每個響應,或做一些繁重的處理之前檢查緩存。
組織這個共享功能的一種方法是設置它 中間件 ——獨立的代碼獨立作用於正常的應用程序請求之前或之後處理程序。 在一個共同的地方去使用ServeMux之間的中間件和應用程序處理程序,以便控制流爲一個HTTP請求的樣子:
ServeMux => Middleware Handler => Application Handler
在這篇文章中,我將解釋如何使自定義中間件,在此模式中,通過一些具體的例子以及運行使用第三方中間件包。
基本原則
製作和使用中間件在根本上是簡單的。 我們希望:
- 實現我們的中間件,使它滿足 http.Handler 接口。
- 建立一個 鏈的處理程序 包含我們的中間件處理程序和正常的應用處理程序,我們可以註冊一個 http.ServeMux 。
希望你已經熟悉下面的方法構建一個處理程序(如果不是,最好讀 https://www.alexedwards.net/blog/a-recap-of-request-handling)。
func messageHandler(message string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(message)
})
}
在這個處理程序(一個簡單的我們將邏輯 w.Write )在一個匿名函數和closing-over message 變量來形成一個閉包。 我們然後將這個閉包轉換爲一個處理程序使用 http.HandlerFunc 適配器並返回它。
我們可以用同樣的方法來創建一個處理程序鏈。 而不是一個字符串傳遞到閉包(如上圖)我們可以通過 鏈中的下一個處理程序 作爲一個變量,然後將控制權移交給下一個處理程序通過調用它 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)
})
}
你會注意到這個中間件功能 func(http.Handler)http.Handler 簽名。 它接受一個處理程序作爲參數,並返回一個處理程序。 這是有用的,有兩個原因:
- 因爲它返回一個處理程序我們可以註冊中間件功能直接與提供的標準ServeMux net/http包。
- 我們可以創建一個任意長度的處理程序鏈嵌套中間件功能在每個其他。 例如:http.Handle("/", middlewareOne(middlewareTwo(finalHandler)))
控制流的說明
讓我們來看看一個精簡的例子和一些中間件,簡單地將日誌消息寫入標準輸出:
文件:main.go
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)
}
運行該應用程序的請求 http://localhost:3000 。 你應該得到類似的日誌輸出:
$ go run main.go
2014/10/13 20:27:36 Executing middlewareOne
2014/10/13 20:27:36 Executing middlewareTwo
2014/10/13 20:27:36 Executing finalHandler
2014/10/13 20:27:36 Executing middlewareTwo again
2014/10/13 20:27:36 Executing middlewareOne again
很明顯看到如何通過控制處理程序鏈的順序嵌套,然後再回來 反方向 。
我們可以停止控制傳播鏈在任何時候通過發行 返回 從一箇中間件處理程序。
在上面的示例中我已經包括了一個條件返回的 middlewareTwo 函數。 嘗試通過訪問 http://localhost:3000 / foo 並再次檢查日誌,你會發現這一次的請求沒有得到進一步的比 middlewareTwo 之前傳遞鏈。
一個適當的例子
好,假設我們正在構建一個服務進程請求包含一個XML的身體。 我們希望創建一些中間件的)檢查請求主體的存在,和b)檢測body以確保它是XML。 如果檢查失敗,我們希望我們的中間件來寫一個錯誤消息並停止請求到達應用程序處理程序。
文件:main.go
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"))
}
這看起來不錯。 讓我們來測試它通過創建一個簡單的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>
使用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
使用第三方中間件
而不是一直在自己的中間件的滾你可能想要使用一個第三方包。 我們要看看幾個: goji/httpauth 和 Gorilla's LoggingHandler 。
goji/httpauth包提供了HTTP基本身份驗證功能。 它有一個 SimpleBasicAuth 輔助它返回一個函數的簽名 func(http.Handler)http.Handler 。 這意味着我們可以在完全相同的方式使用它作爲我們的定製中間件。
$ go get github.com/goji/httpauth
文件:main.go
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"))
}
如果你運行這個例子,你應該得到你所期望的反應的有效和無效憑證:
$ 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
go get github.com/gorilla/handlers
文件:main.go
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"))
}
在這樣一個微不足道的情況下我們的代碼是相當清楚的。 但是如果我們想使用LoggingHandler中間件鏈的一部分嗎? 我們可以很容易地得到一個聲明是這樣的……
http.Handle("/", handlers.LoggingHandler(logFile, authHandler(enforceXMLHandler(finalHandler))))
弄清楚的一個方法是通過創建一個構造函數(我們叫它 myLoggingHandler )簽名 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)
}
如果你運行這個應用程序和做一些請求 server.log 文件應該是這樣的:
$ 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
如果你感興趣,這是一個要點 三種中間件處理程序 從這篇文章結合一個例子。
邊注:注意 Gorilla LoggingHandler記錄響應狀態( 200 )和響應的長度( 2) 日誌中。 這是有趣的。 upstream 中間件怎麼知道響應主體由我們的應用處理程序嗎?
它通過定義它自己的 responseLogger 類型的包裝 http.ResponseWriter ,創建自定義 responseLogger.Write() 和 responseLogger.WriteHeader() 方法。 這些方法不僅寫響應,而且存儲大小和地位,以便日後檢查。 Gorilla LoggingHandler傳遞 responseLogger 到鏈中的下一個處理程序,而不是正常的 http.ResponseWriter 。
額外的工具
https://github.com/justinas/alice
Alice by Justinas Stankevičius 是一個聰明的和非常輕量級的包提供了一些鏈接中間件處理程序的語法糖。 在最基本的Alice讓你重寫這個:
http.Handle("/", myLoggingHandler(authHandler(enforceXMLHandler(finalHandler))))
是這樣的:
http.Handle("/", alice.New(myLoggingHandler, authHandler, enforceXMLHandler).Then(finalHandler))
至少在我眼裏,這段代碼稍微清晰的理解。 但是,Alice 真正的好處是,它允許您指定一個處理程序鏈一旦和重用它爲多個路線。 像這樣:
stdChain := alice.New(myLoggingHandler, authHandler, enforceXMLHandler)
http.Handle("/foo", stdChain.Then(fooHandler))
http.Handle("/bar", stdChain.Then(barHandler))