Go 如何處理 HTTP 請求?掌握這兩點即可

2019-12-04   Go語言中文網

使用 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)
}

讓我們快速介紹一下:

  • 在 main 函數中,我們使用http.NewServeMux[6] 函數創建了一個空的 ServeMux。
  • 然後我們使用http.RedirectHandler[7] 函數創建一個新的 handler。該 handler 將其接收的所有請求 307 重定向到 http://example.org。
  • 接下來我們使用mux.Handle[8] 函數向我們的新 ServeMux 註冊它,因此它充當 URL 路徑 /foo 的所有傳入請求的 handler。
  • 最後,我們創建一個新服務並使用http.ListenAndServe[9] 函數開始監聽傳入的請求,並傳入 ServeMux 給這個方法以匹配請求。

繼續運行應用程式:

$ 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/


推薦閱讀

  • sync.Pool 一定會提升性能嗎?建議你先學習一下它的設計
  • Go 號稱幾行代碼開啟一個 HTTP Server,底層都做了什麼?




喜歡本文的朋友,歡迎關注「Go語言中文網」:

Go語言中文網啟用微信學習交流群,歡迎加微信:274768166