使用 Go 處理 HTTP 請求主要涉及兩件事:ServeMuxes 和 Handlers。
ServeMux[1] 本質上是一個 HTTP 請求路由器(或多路復用器)。它將傳入的請求與預定義的 URL 路徑列表進行比較,並在找到匹配時調用路徑的關聯 handler。
handler 負責寫入響應頭和響應體。幾乎任何對象都可以是 handler,只要它滿足http.Handler[2] 接口即可。在非專業術語中,這僅僅意味著它必須是一個擁有以下簽名的 ServeHTTP 方法:
ServeHTTP(http.ResponseWriter, *http.Request)
Go 的 HTTP 包附帶了一些函數來生成常用的 handler,例如FileServer[3],NotFoundHandler[4] 和RedirectHandler[5]。讓我們從一個簡單的例子開始:
$ mkdir handler-example
$ cd handler-example
$ touch main.go
File: main.go
package main
import (
\t"log"
\t"net/http"
)
func main() {
\tmux := http.NewServeMux()
\trh := http.RedirectHandler("http://example.org", 307)
\tmux.Handle("/foo", rh)
\tlog.Println("Listening...")
\thttp.ListenAndServe(":3000", mux)
}
讓我們快速介紹一下:
繼續運行應用程式:
$ go run main.go
Listening...
並在瀏覽器中訪問http://localhost:3000/foo[10]。你會發現請求已經被成功重定向。
你可能已經注意到了一些有趣的東西:ListenAndServe 函數的簽名是 ListenAndServe(addr string, handler Handler),但我們傳遞了一個 ServeMux 作為第二個參數。
能這麼做是因為 ServeMux 類型也有一個 ServeHTTP 方法,這意味著它也滿足 Handler 接口。
對我而言,它只是將 ServeMux 視為一種特殊的 handler,而不是把響應本身通過第二個 handler 參數傳遞給請求。這不像剛剛聽說時那麼驚訝 - 將 handler 連結在一起在 Go 中相當普遍。
自定義 handler
我們創建一個自定義 handler,它以當前本地時間的指定格式響應:
type timeHandler struct {
\tformat string
}
func (th *timeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
\ttm := time.Now().Format(th.format)
\tw.Write([]byte("The time is: " + tm))
}
這裡確切的代碼並不太重要。
真正重要的是我們有一個對象(在該示例中它是一個 timeHandler 結構,它同樣可以是一個字符串或函數或其他任何東西),並且我們已經實現了一個帶有簽名 ServeHTTP(http.ResponseWriter, *http.Request) 的方法。這就是我們實現一個 handler 所需的全部內容。
讓我們將其嵌入一個具體的例子中:
File: main.go
package main
import (
\t"log"
\t"net/http"
\t"time"
)
type timeHandler struct {
\tformat string
}
func (th *timeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
\ttm := time.Now().Format(th.format)
\tw.Write([]byte("The time is: " + tm))
}
func main() {
\tmux := http.NewServeMux()
\tth := &timeHandler{format: time.RFC1123}
\tmux.Handle("/time", th)
\tlog.Println("Listening...")
\thttp.ListenAndServe(":3000", mux)
}
在 main 函數中,我們使用 & 符號生成指針,用與普通結構完全相同的方式初始化 timeHandler。然後,與前面的示例一樣,我們使用 mux.Handle 函數將其註冊到我們的 ServeMux。
現在,當我們運行應用程式時,ServeMux 會將任何通過 /time 路徑的請求直接傳遞給我們的 timeHandler.ServeHTTP 方法。
試一試:http://localhost:3000/time[11]。
另請注意,我們可以輕鬆地在多個路徑中重複使用 timeHandler:
func main() {
\tmux := http.NewServeMux()
\tth1123 := &timeHandler{format: time.RFC1123}
\tmux.Handle("/time/rfc1123", th1123)
\tth3339 := &timeHandler{format: time.RFC3339}
\tmux.Handle("/time/rfc3339", th3339)
\tlog.Println("Listening...")
\thttp.ListenAndServe(":3000", mux)
}
普通函數作為 handler
對於簡單的情況(如上例),定義新的自定義類型和 ServeHTTP 方法感覺有點囉嗦。讓我們看看另一個方法,我們利用 Go 的http.HandlerFunc[12] 類型來使正常的函數滿足 Handler 接口。
任何具有簽名 func(http.ResponseWriter, *http.Request) 的函數都可以轉換為 HandlerFunc 類型。這很有用,因為 HandleFunc 對象帶有一個內置的 ServeHTTP 方法 - 這非常巧妙且方便 - 執行原始函數的內容。
如果這聽起來令人費解,請嘗試查看相關的原始碼[13]。你將看到它是一種讓函數滿足 Handler 接口的非常簡潔的方法。
我們使用這種方法來重寫 timeHandler 應用程式:
File: main.go
package main
import (
\t"log"
\t"net/http"
\t"time"
)
func timeHandler(w http.ResponseWriter, r *http.Request) {
\ttm := time.Now().Format(time.RFC1123)
\tw.Write([]byte("The time is: " + tm))
}
func main() {
\tmux := http.NewServeMux()
\t// Convert the timeHandler function to a HandlerFunc type
\tth := http.HandlerFunc(timeHandler)
\t// And add it to the ServeMux
\tmux.Handle("/time", th)
\tlog.Println("Listening...")
\thttp.ListenAndServe(":3000", mux)
}
事實上,將函數轉換為 HandlerFunc 類型,然後將其添加到 ServeMux 的情況比較常見,Go 提供了一個快捷的轉換方法:mux.HandleFunc[14] 方法。
如果我們使用這個轉換方法,main() 函數將是這個樣子:
func main() {
\tmux := http.NewServeMux()
\tmux.HandleFunc("/time", timeHandler)
\tlog.Println("Listening...")
\thttp.ListenAndServe(":3000", mux)
}
大多數時候使用這樣的 handler 很有效。但是當事情變得越來越複雜時,將會受限。
你可能已經注意到,與之前的方法不同,我們必須在 timeHandler 函數中對時間格式進行硬編碼。當我們想要將信息或變量從 main() 傳遞給 handler 時會發生什麼?
一個簡潔的方法是將我們的 handler 邏輯放入一個閉包中,把我們想用的變量包起來:
File: main.go
package main
import (
\t"log"
\t"net/http"
\t"time"
)
func timeHandler(format string) http.Handler {
\tfn := func(w http.ResponseWriter, r *http.Request) {
\t\ttm := time.Now().Format(format)
\t\tw.Write([]byte("The time is: " + tm))
\t}
\treturn http.HandlerFunc(fn)
}
func main() {
\tmux := http.NewServeMux()
\tth := timeHandler(time.RFC1123)
\tmux.Handle("/time", th)
\tlog.Println("Listening...")
\thttp.ListenAndServe(":3000", mux)
}
timeHandler 函數現在有一點點不同。現在使用它來返回 handler,而不是將函數強制轉換為 handler(就像我們之前所做的那樣)。能這麼做有兩個關鍵點。
首先它創建了一個匿名函數 fn,它訪問形成閉包的 format 變量。無論我們如何處理閉包,它總是能夠訪問它作用域下所創建的局部變量 - 在這種情況下意味著它總是可以訪問 format 變量。
其次我們的閉包有簽名為 func(http.ResponseWriter, *http.Request) 的函數。你可能還記得,這意味著我們可以將其轉換為 HandlerFunc 類型(以便它滿足 Handler 接口)。然後我們的 timeHandler 函數返回這個轉換後的閉包。
在這個例子中,我們僅僅將一個簡單的字符串傳遞給 handler。但在實際應用程式中,您可以使用此方法傳遞資料庫連接,模板映射或任何其他應用程式級的上下文。它是全局變量的一個很好的替代方案,並且可以使測試的自包含 handler 變得更整潔。
你可能還會看到相同的模式,如下所示:
func timeHandler(format string) http.Handler {
\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
\t\ttm := time.Now().Format(format)
\t\tw.Write([]byte("The time is: " + tm))
\t})
}
或者在返回時使用隱式轉換為 HandlerFunc 類型:
func timeHandler(format string) http.HandlerFunc {
\treturn func(w http.ResponseWriter, r *http.Request) {
\t\ttm := time.Now().Format(format)
\t\tw.Write([]byte("The time is: " + tm))
\t}
}
DefaultServeMux
你可能已經看到過很多地方提到的 DefaultServeMux,包括最簡單的 Hello World 示例到 Go 原始碼。
我花了很長時間才意識到它並不特別。DefaultServeMux 只是一個普通的 ServeMux,就像我們已經使用的那樣,默認情況下在使用 HTTP 包時會實例化。以下是 Go 原始碼中的相關行:
var DefaultServeMux = NewServeMux()
通常,你不應使用 DefaultServeMux,因為它會帶來安全風險。
由於 DefaultServeMux 存儲在全局變量中,因此任何程序包都可以訪問它並註冊路由 - 包括應用程式導入的任何第三方程序包。如果其中一個第三方軟體包遭到破壞,他們可以使用 DefaultServeMux 向 Web 公開惡意 handler。
因此,根據經驗,避免使用 DefaultServeMux 是一個好主意,取而代之使用你自己的本地範圍的 ServeMux,就像我們到目前為止一樣。但如果你決定使用它……
HTTP 包提供了一些使用 DefaultServeMux 的便捷方式:http.Handle[15] 和http.HandleFunc[16]。這些與我們已經看過的同名函數完全相同,不同之處在於它們將 handler 添加到 DefaultServeMux 而不是你自己創建的 handler。
此外,如果沒有提供其他 handler(即第二個參數設置為 nil),ListenAndServe 將退回到使用 DefaultServeMux。
因此,作為最後一步,讓我們更新我們的 timeHandler 應用程式以使用 DefaultServeMux:
File: main.go
package main
import (
\t"log"
\t"net/http"
\t"time"
)
func timeHandler(format string) http.Handler {
\tfn := func(w http.ResponseWriter, r *http.Request) {
\t\ttm := time.Now().Format(format)
\t\tw.Write([]byte("The time is: " + tm))
\t}
\treturn http.HandlerFunc(fn)
}
func main() {
\t// Note that we skip creating the ServeMux...
\tvar format string = time.RFC1123
\tth := timeHandler(format)
\t// We use http.Handle instead of mux.Handle...
\thttp.Handle("/time", th)
\tlog.Println("Listening...")
\t// And pass nil as the handler to ListenAndServe.
\thttp.ListenAndServe(":3000", nil)
}
如果你喜歡這篇博文,請不要忘記查看我的新書《用 Go 構建專 業的 Web 應用程式》[17] !
在推特上關注我 @ajmedwards[18]。
此文章中的所有代碼都可以在MIT Licence[19] 許可下免費使用。
via: https://www.alexedwards.net/blog/a-recap-of-request-handling
作者:Alex Edwards[20]譯者:咔嘰咔嘰[21]校對:polaris1119[22]
本文由 GCTT[23] 原創編譯,Go 中文網[24] 榮譽推出
文中連結
[1]
ServeMux: https://docs.studygolang.com/pkg/net/http/#ServeMux
[2]
http.Handler: https://docs.studygolang.com/pkg/net/http/#Handler
[3]
FileServer: https://docs.studygolang.com/pkg/net/http/#FileServer
[4]
NotFoundHandler: https://docs.studygolang.com/pkg/net/http/#NotFoundHandler
[5]
RedirectHandler: https://docs.studygolang.com/pkg/net/http/#RedirectHandler
[6]
http.NewServeMux: https://docs.studygolang.com/pkg/net/http/#NewServeMux
[7]
http.RedirectHandler: https://docs.studygolang.com/pkg/net/http/#RedirectHandler
[8]
mux.Handle: https://docs.studygolang.com/pkg/net/http/#ServeMux.Handle
[9]
http.ListenAndServe: https://docs.studygolang.com/pkg/net/http/#ListenAndServe
[10]
http://localhost:3000/foo: http://localhost:3000/foo
[11]
http://localhost:3000/time: http://localhost:3000/time
[12]
http.HandlerFunc: https://docs.studygolang.com/pkg/net/http/#HandlerFunc
[13]
相關的原始碼: https://golang.org/src/net/http/server.go?s=57023:57070#L1904
[14]
mux.HandleFunc: https://docs.studygolang.com/pkg/net/http/#ServeMux.HandleFunc
[15]
http.Handle: https://docs.studygolang.com/pkg/net/http/#Handle
[16]
http.HandleFunc: https://docs.studygolang.com/pkg/net/http/#HandleFunc
[17]
《用 Go 構建專業的 Web 應用程式》: https://lets-go.alexedwards.net/
[18]
@ajmedwards: https://twitter.com/ajmedwards
[19]
MIT Licence: http://opensource.org/licenses/MIT
[20]
Alex Edwards: https://www.alexedwards.net/
[21]
咔嘰咔嘰: https://github.com/watermelo
[22]
polaris1119: https://github.com/polaris1119
[23]
GCTT: https://github.com/studygolang/GCTT
[24]
Go 中文網: https://studygolang.com/
推薦閱讀
喜歡本文的朋友,歡迎關注「Go語言中文網」:
Go語言中文網啟用微信學習交流群,歡迎加微信:274768166