這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。在構建 Web 應用程式時,可能需要為許多(甚至全部)HTTP 要求運行一些公用的函數。你可能需要對每個請求進行記錄,對每個響應進行壓縮,或者在執行一些重要的處理之前檢查一下緩衝。組織這種公用函數的一種方法是將其設定為中介層 - 自包含代碼,它們在正常應用處理常式之前或之後,獨立地處理請求。在 Go 中,使用中介層的常見位置在 ServeMux 和應用程式處理之間,因此通常對 HTTP 要求的控制流程程如下所示:`ServeMux => Middleware Handler => Application Handler`在這篇文章中,我將解釋如何使自訂中介層在這種模式下工作,以及如何使用第三方中介層包的一些具體的樣本。## 基本原則(The Basic Principles)在 Go 中製作和使用中介層很簡單。我們可以設想:實現我們自己的中介層,使其滿足 http.Handler 介面。構建一個包含我們的中介層處理常式和我們的普通應用處理常式的處理鏈,我們可以使用它來註冊 http.ServeMux。我會解釋如何做。希望你已經熟悉下面構造一個處理常式的方法(如果沒有,最好在繼續閱讀前,看下這個底層的程式)。```gofunc messageHandler(message string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(message) })}```在這個處理常式中,我們將我們的邏輯(一個簡單的 `w.Write`)放在匿名函數中,並封裝 `message` 變數以形成閉包。然後我們使用 http.HandlerFunc 適配器並將其返回,將此閉包轉換為處理常式。我們可以使用這種相同的方法來建立一系列的處理常式。我們將鏈中的下一個處理常式作為變數傳遞給閉包(而不是像上面那樣),然後通過調用 ServeHTTP() 方法將控制權轉移給下一個處理常式。這為我們提供了構建中介層的完整的模式:```gofunc 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` 簽名。它接受一個處理常式作為參數並返回一個處理常式。這是有用的,原因有兩個:因為它返回一個處理常式,我們可以直接使用 net/http 軟體包提供的標準 ServeMux 註冊中介層函數。通過將中介層函數嵌套在一起,我們可以建立一個任意長的處理常式鏈。例如:`http.Handle("/", middlewareOne(middlewareTwo(finalHandler)))`## 控制流程程說明(Illustrating the Flow of Control)讓我們看一個簡單的例子,它帶有一些只需將日誌訊息寫入標準輸出的中介層函數:```File: main.go``````gopackage mainimport ( "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.go2014/10/13 20:27:36 Executing middlewareOne2014/10/13 20:27:36 Executing middlewareTwo2014/10/13 20:27:36 Executing finalHandler2014/10/13 20:27:36 Executing middlewareTwo again2014/10/13 20:27:36 Executing middlewareOne again```很明顯,可以看到如何通過處理常式鏈按照嵌套順序傳遞控制權,然後再以相反的方向返回。任何時候,我們都可以通過在中介層處理常式中返回,來停止鏈傳遞的控制項。在上面的例子中,我在中介層中包含了一個條件返回函數。通過訪問 `http://localhost:3000/foo` 再次嘗試,並檢查日誌 - 你會發現此次請求不會通過中介層進一步傳遞到備份鏈。## 通過一個合適的例子來瞭解如何?(Understood. How About a Proper Example?)好了。假設我們正在構建一個處理本文中包含 XML 請求的服務。我們想要建立一些中介層,它們 a)檢查請求體是否存在,b)嗅探以確保它是 XML(格式)。如果其中任何一項檢查失敗,我們希望我們的中介層寫入錯誤訊息並停止將請求傳遞給我們的應用處理常式。```File: main.go``````gopackage mainimport ( "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:3000HTTP/1.1 400 Bad RequestContent-Type: text/plain; charset=utf-8Content-Length: 12Bad Request$ curl -i -d "This is not XML" localhost:3000HTTP/1.1 415 Unsupported Media TypeContent-Type: text/plain; charset=utf-8Content-Length: 23Unsupported Media Type$ curl -i -d @books.xml localhost:3000HTTP/1.1 200 OKDate: Fri, 17 Oct 2014 13:42:10 GMTContent-Length: 2Content-Type: text/plain; charset=utf-8OK```## 使用第三方中介層(Using Third-Party Middleware)基本上你想直接使用第三方軟體包而不是自己寫中介層。我們將在這裡看到一對(第三方軟體包):[goji/httpauth](http://elithrar.github.io/article/httpauth-basic-auth-for-go/) 和 Gorilla 的 [LoggingHandler](http://www.gorillatoolkit.org/pkg/handlers#LoggingHandler)。goji/httpauth 包提供了 HTTP 基本的認證功能。它有一個 [SimpleBasicAuth](https://godoc.org/github.com/goji/httpauth#SimpleBasicAuth) helper,它返回一個帶有簽名的 `func (http.Handler) http.Handler` 函數。這意味著我們可以像我們定製的中介層一樣(的方式)使用它。```$ go get github.com/goji/httpauth``````File: main.go``````gopackage mainimport ( "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:3000HTTP/1.1 200 OKContent-Length: 2Content-Type: text/plain; charset=utf-8OK$ curl -i username:wrongpassword@localhost:3000HTTP/1.1 401 UnauthorizedContent-Type: text/plain; charset=utf-8Www-Authenticate: Basic realm=""Restricted""Content-Length: 13Unauthorized```Gorilla 的 LoggingHandler - 它記錄了 [Apache 風格的日誌](http://httpd.apache.org/docs/1.3/logs.html#common) - 有點不一樣。它使用簽名 `func(out io.Writer, h http.Handler) http.Handler`,所以它不僅需要下一個處理常式,還需要將日誌寫入的 io.Writer。以下是一個簡單的例子,我們將日誌寫入 `server.log` 檔案:```bashgo get github.com/gorilla/handlers``````File: main.go``````gopackage mainimport ( "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`。這將使我們能夠與其他中介層更加簡潔地嵌套在一起:```gofunc 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.log127.0.0.1 - - [21/Oct/2014:18:56:43 +0100] "GET / HTTP/1.1" 200 2127.0.0.1 - - [21/Oct/2014:18:56:36 +0100] "POST / HTTP/1.1" 200 2127.0.0.1 - - [21/Oct/2014:18:56:43 +0100] "PUT / HTTP/1.1" 200 2```如果你有興趣,可以參考這篇文章中的三個中介層處理常式。附註:請注意,Gorilla LoggingHandler 正在記錄日誌中的響應狀態(`200`)和響應長度(`2`)。這很有趣。上遊的日誌記錄中介層是如何知道我們的應用處理常式編寫的響應的?它通過定義自己的 `responseLogger` 類來封裝 `http.ResponseWriter`,並建立自訂的 `reponseLogger.Write()` 和 `reponseLogger.WriteHeader()` 方法。這些方法不僅可以編寫響應,還可以儲存大小和狀態供以後檢查。Gorilla 的 LoggingHandler 將 `reponseLogger` 傳遞給鏈中的下一個處理常式,而不是普通的 `http.ResponseWriter`。## 附加工具(Additional Tools)[由 Justinas Stankevičius 編寫的 Alice](https://github.com/justinas/alice) 是一個非常聰明並且輕量級的包,它為串連中介層處理常式提供了一些文法糖。在最基礎的方面,Alice 允許你重寫這個:`http.Handle("/", myLoggingHandler(authHandler(enforceXMLHandler(finalHandler))))`為這個:`http.Handle("/", alice.New(myLoggingHandler, authHandler, enforceXMLHandler).Then(finalHandler))`至少在我看來,這些代碼一眼就能看清楚這一點。但是,Alice 的真正好處是它可以讓你指定一個處理常式鏈並將其重複用於多個路由。像這樣:```gostdChain := alice.New(myLoggingHandler, authHandler, enforceXMLHandler)http.Handle("/foo", stdChain.Then(fooHandler))http.Handle("/bar", stdChain.Then(barHandler))```
via: http://www.alexedwards.net/blog/making-and-using-middleware
作者:TIAGO KATCIPIS 譯者:gogeof 校對:polaris1119
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
334 次點擊