概述
一個典型的 Go Web 程序結構如下,摘自《Go Web 編程》:
- 客戶端發送請求;
- 服務器中的多路復用器收到請求;
- 多路復用器根據請求的 URL 找到注冊的處理器,將請求交由處理器處理;
- 處理器執行程序邏輯,必要時與數據庫進行交互,得到處理結果;
- 處理器調用模板引擎將指定的模板和上一步得到的結果渲染成客戶端可識別的數據格式(通常是 HTML);
- 最后將數據通過響應返回給客戶端;
- 客戶端拿到數據,執行對應的操作,如渲染出來呈現給用戶。
本文介紹如何創建多路復用器,如何注冊處理器,最后再簡單介紹一下 URL 匹配。我們以上一篇文章中的"Hello World"程序作為基礎。
package main
import (
"fmt"
"log"
"net/http"
)
func hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World")
}
func main() {
http.HandleFunc("/", hello)
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
多路復用器
默認多路復用器
net/http 包為了方便我們使用,內置了一個默認的多路復用器DefaultServeMux
。定義如下:
// src/net/http/server.go
// DefaultServeMux is the default ServeMux used by Serve.
var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux
這里給大家介紹一下 Go 標准庫代碼的組織方式,便於大家對照。
- Windows上,Go 語言的默認安裝目錄為
C:\Go
,即GOROOT
; GOROOT
下有一個 src 目錄,標庫庫的代碼都在這個目錄中;- 每個包有一個單獨的目錄,例如 fmt 包在
src/fmt
目錄中; - 子包在其父包的子目錄中,例如 net/http 包在
src/net/http
目錄中。
net/http 包中很多方法都在內部調用DefaultServeMux
的對應方法,如HandleFunc
。我們知道,HandleFunc
是為指定的 URL 注冊一個處理器(准確來說,hello
是處理器函數,見下文)。其內部實現如下:
// src/net/http/server.go
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}
實際上,http.HandleFunc
方法是將處理器注冊到DefaultServeMux
中的。
另外,我們使用 ":8080" 和 nil
作為參數調用http.ListenAndServe
時,會創建一個默認的服務器:
// src/net/http/server.go
func ListenAndServe(addr string, handler Handler) {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
這個服務器默認使用DefaultServeMux
來處理器請求:
type serverHandler struct {
srv *Server
}
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
handler := sh.srv.Handler
if handler == nil {
handler = DefaultServeMux
}
handler.ServeHTTP(rw, req)
}
服務器收到的每個請求會調用對應多路復用器(即ServeMux
)的ServeHTTP
方法。在ServeMux
的ServeHTTP
方法中,根據 URL 查找我們注冊的處理器,然后將請求交由它處理。
雖然默認的多路復用器使用起來很方便,但是在生產環境中不建議使用。由於DefaultServeMux
是一個全局變量,所有代碼,包括第三方代碼都可以修改它。
有些第三方代碼會在DefaultServeMux
注冊一些處理器,這可能與我們注冊的處理器沖突。
比較推薦的做法是自己創建多路復用器。
創建多路復用器
創建多路復用器也比較簡單,直接調用http.NewServeMux
方法即可。然后,在新創建的多路復用器上注冊處理器:
package main
import (
"fmt"
"log"
"net/http"
)
func hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World")
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", hello)
server := &http.Server{
Addr: ":8080",
Handler: mux,
}
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
上面代碼的功能與 "Hello World" 程序相同。這里我們還自己創建了服務器對象。通過指定服務器的參數,我們可以創建定制化的服務器。
server := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 1 * time.Second,
WriteTimeout: 1 * time.Second,
}
在上面代碼,我們創建了一個讀超時和寫超時均為 1s 的服務器。
處理器和處理器函數
上文中提到,服務器收到請求后,會根據其 URL 將請求交給相應的處理器處理。處理器是實現了Handler
接口的結構,Handler
接口定義在 net/http 包中:
// src/net/http/server.go
type Handler interface {
func ServeHTTP(w Response.Writer, r *Request)
}
我們可以定義一個實現該接口的結構,注冊這個結構類型的對象到多路復用器中:
package main
import (
"fmt"
"log"
"net/http"
)
type GreetingHandler struct {
Language string
}
func (h GreetingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "%s", h.Language)
}
func main() {
mux := http.NewServeMux()
mux.Handle("/chinese", GreetingHandler{Language: "你好"})
mux.Handle("/english", GreetingHandler{Language: "Hello"})
server := &http.Server {
Addr: ":8080",
Handler: mux,
}
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
與前面的代碼有所不同,上段代碼中,定義了一個實現Handler
接口的結構GreetingHandler
。然后,創建該結構的兩個對象,分別將它注冊到多路復用器的/hello
和/world
路徑上。注意,這里注冊使用的是Handle
方法,注意與HandleFunc
方法對比。
啟動服務器之后,在瀏覽器的地址欄中輸入localhost:8080/chinese
,瀏覽器中將顯示你好
,輸入localhost:8080/english
將顯示Hello
。
雖然,自定義處理器這種方式比較靈活,強大,但是需要定義一個新的結構,實現ServeHTTP
方法,還是比較繁瑣的。為了方便使用,net/http 包提供了以函數的方式注冊處理器,即使用HandleFunc
注冊。函數必須滿足簽名:func (w http.ResponseWriter, r *http.Request)
。
我們稱這個函數為處理器函數。我們的 "Hello World" 程序中使用的就是這種方式。HandleFunc
方法內部,會將傳入的處理器函數轉換為HandlerFunc
類型。
// src/net/http/server.go
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if handler == nil {
panic("http: nil handler")
}
mux.Handle(pattern, HandlerFunc(handler))
}
HandlerFunc
是底層類型為func (w ResponseWriter, r *Request)
的新類型,它可以自定義其方法。由於HandlerFunc
類型實現了Handler
接口,所以它也是一個處理器類型,最終使用Handle
注冊。
// src/net/http/server.go
type HandlerFunc func(w *ResponseWriter, r *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
注意,這幾個接口和方法名很容易混淆,這里再強調一下:
Handler
:處理器接口,定義在 net/http 包中。實現該接口的類型,其對象可以注冊到多路復用器中;Handle
:注冊處理器的方法;HandleFunc
:注冊處理器函數的方法;HandlerFunc
:底層類型為func (w ResponseWriter, r *Request)
的新類型,實現了Handler
接口。它連接了處理器函數與處理器。
URL 匹配
一般的 Web 服務器有非常多的 URL 綁定,不同的 URL 對應不同的處理器。但是服務器是怎么決定使用哪個處理器的呢?例如,我們現在綁定了 3 個 URL,/
和/hello
和/hello/world
。
顯然,如果請求的 URL 為/
,則調用/
對應的處理器。如果請求的 URL 為/hello
,則調用/hello
對應的處理器。如果請求的 URL 為/hello/world
,則調用/hello/world
對應的處理器。
但是,如果請求的是/hello/others
,那么使用哪一個處理器呢? 匹配遵循以下規則:
- 首先,精確匹配。即查找是否有
/hello/others
對應的處理器。如果有,則查找結束。如果沒有,執行下一步; - 將路徑中最后一個部分去掉,再次查找。即查找
/hello/
對應的處理器。如果有,則查找結束。如果沒有,繼續執行這一步。即查找/
對應的處理器。
這里有一個注意點,如果注冊的 URL 不是以/
結尾的,那么它只能精確匹配請求的 URL。反之,即使請求的 URL 只有前綴與被綁定的 URL 相同,ServeMux
也認為它們是匹配的。
這也是為什么上面步驟進行到/hello/
時,不能匹配/hello
的原因。因為/hello
不以/
結尾,必須要精確匹配。
如果,我們綁定的 URL 為/hello/
,那么當服務器找不到與/hello/others
完全匹配的處理器時,就會退而求其次,開始尋找能夠與/hello/
匹配的處理器。
看下面的代碼:
package main
import (
"fmt"
"log"
"net/http"
)
func indexHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "This is the index page")
}
func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "This is the hello page")
}
func worldHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "This is the world page")
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", indexHandler)
mux.HandleFunc("/hello", helloHandler)
mux.HandleFunc("/hello/world", worldHandler)
server := &http.Server{
Addr: ":8080",
Handler: mux,
}
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
- 瀏覽器請求
localhost:8080/
將返回"This is the index page"
,因為/
精確匹配;
- 瀏覽器請求
localhost:8080/hello
將返回"This is the hello page"
,因為/hello
精確匹配;
- 瀏覽器請求
localhost:8080/hello/
將返回"This is the index page"
。注意這里不是hello
,因為綁定的/hello
需要精確匹配,而請求的/hello/
不能與之精確匹配。故而向上查找到/
;
- 瀏覽器請求
localhost:8080/hello/world
將返回"This is the world page"
,因為/hello/world
精確匹配;
- 瀏覽器請求
localhost:8080/hello/world/
將返回"This is the index page"
,查找步驟為/hello/world/
(不能與/hello/world
精確匹配)->/hello/
(不能與/hello/
精確匹配)->/
;
- 瀏覽器請求
localhost:8080/hello/other
將返回"This is the index page"
,查找步驟為/hello/others
->/hello/
(不能與/hello
精確匹配)->/
;
如果注冊時,將/hello
改為/hello/
,那么請求localhost:8080/hello/
和localhost:8080/hello/world/
都將返回"This is the hello page"
。自己試試吧!
思考:
使用/hello/
注冊處理器時,localhost:8080/hello/
返回什么?
總結
本文介紹了 Go Web 程序的基本結構。Go Web 的基本形式如下:
package main
import (
"fmt"
"log"
"net/http"
)
func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World")
}
type greetingHandler struct {
Name string
}
func (h greetingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %s", h.Name)
}
func main() {
mux := http.NewServeMux()
// 注冊處理器函數
mux.HandleFunc("/hello", helloHandler)
// 注冊處理器
mux.Handle("/greeting/golang", greetingHandler{Name: "Golang"})
server := &http.Server {
Addr: ":8080",
Handler: mux,
}
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
后續文章中大部分程序只是在此基礎上增加處理器或處理器函數並注冊到相應的 URL 中而已。處理器和處理器函數可以只使用一種或兩者都使用。注意,為了方便,命名中我都加上了Handler
。
參考資料
我
歡迎關注我的微信公眾號【GoUpUp】,共同學習,一起進步~
本文由博客一文多發平台 OpenWrite 發布!