原文鏈接:https://gopl-zh.github.io/ch7/ch7-07.html
在第一章中,我們粗略的了解了怎么用net/http包去實現(xiàn)網(wǎng)絡客戶端(§1.5)和服務器(§1.7)。在這個小節(jié)中,我們會對那些基于http.Handler接口的服務器API做更進一步的學習:
net/http
package http
type Handler interface {
ServeHTTP(w ResponseWriter, r *Request)
}
func ListenAndServe(address string, h Handler) error
ListenAndServe函數(shù)需要一個例如“l(fā)ocalhost:8000”的服務器地址,和一個所有請求都可以分派的Handler接口實例。它會一直運行,直到這個服務因為一個錯誤而失?。ɑ蛘邌邮。?,它的返回值一定是一個非空的錯誤。
想象一個電子商務網(wǎng)站,為了銷售,將數(shù)據(jù)庫中物品的價格映射成美元。下面這個程序可能是能想到的最簡單的實現(xiàn)了。它將庫存清單模型化為一個命名為database的map類型,我們給這個類型一個ServeHttp方法,這樣它可以滿足http.Handler接口。這個handler會遍歷整個map并輸出物品信息。
gopl.io/ch7/http1
func main() {
db := database{"shoes": 50, "socks": 5}
log.Fatal(http.ListenAndServe("localhost:8000", db))
}
type dollars float32
func (d dollars) String() string { return fmt.Sprintf("$%.2f", d) }
type database map[string]dollars
func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
for item, price := range db {
fmt.Fprintf(w, "%s: %s\n", item, price)
}
}
如果我們啟動這個服務
$ go build gopl.io/ch7/http1
$ ./http1 &
然后用1.5節(jié)中的獲取程序(如果你更喜歡可以使用web瀏覽器)來連接服務器,我們得到下面的輸出:
$ go build gopl.io/ch1/fetch
$ ./fetch http://localhost:8000
shoes: $50.00
socks: $5.00
目前為止,這個服務器不考慮URL,只能為每個請求列出它全部的庫存清單。更真實的服務器會定義多個不同的URL,每一個都會觸發(fā)一個不同的行為。讓我們使用/list來調(diào)用已經(jīng)存在的這個行為并且增加另一個/price調(diào)用表明單個貨品的價格,像這樣/price?item=socks來指定一個請求參數(shù)。
gopl.io/ch7/http2
func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
switch req.URL.Path {
case "/list":
for item, price := range db {
fmt.Fprintf(w, "%s: %s\n", item, price)
}
case "/price":
item := req.URL.Query().Get("item")
price, ok := db[item]
if !ok {
w.WriteHeader(http.StatusNotFound) // 404
fmt.Fprintf(w, "no such item: %q\n", item)
return
}
fmt.Fprintf(w, "%s\n", price)
default:
w.WriteHeader(http.StatusNotFound) // 404
fmt.Fprintf(w, "no such page: %s\n", req.URL)
}
}
現(xiàn)在handler基于URL的路徑部分(req.URL.Path)來決定執(zhí)行什么邏輯。如果這個handler不能識別這個路徑,它會通過調(diào)用w.WriteHeader(http.StatusNotFound)返回客戶端一個HTTP錯誤;這個檢查應該在向w寫入任何值前完成。(順便提一下,http.ResponseWriter是另一個接口。它在io.Writer上增加了發(fā)送HTTP相應頭的方法。)等效地,我們可以使用實用的http.Error函數(shù):
msg := fmt.Sprintf("no such page: %s\n", req.URL)
http.Error(w, msg, http.StatusNotFound) // 404
/price的case會調(diào)用URL的Query方法來將HTTP請求參數(shù)解析為一個map,或者更準確地說一個net/url包中url.Values(§6.2.1)類型的多重映射。然后找到第一個item參數(shù)并查找它的價格。如果這個貨品沒有找到會返回一個錯誤。
這里是一個和新服務器會話的例子:
$ go build gopl.io/ch7/http2
$ go build gopl.io/ch1/fetch
$ ./http2 &
$ ./fetch http://localhost:8000/list
shoes: $50.00
socks: $5.00
$ ./fetch http://localhost:8000/price?item=socks
$5.00
$ ./fetch http://localhost:8000/price?item=shoes
$50.00
$ ./fetch http://localhost:8000/price?item=hat
no such item: "hat"
$ ./fetch http://localhost:8000/help
no such page: /help
顯然我們可以繼續(xù)向ServeHTTP方法中添加case,但在一個實際的應用中,將每個case中的邏輯定義到一個分開的方法或函數(shù)中會很實用。此外,相近的URL可能需要相似的邏輯;例如幾個圖片文件可能有形如/images/*.png的URL。因為這些原因,net/http包提供了一個請求多路器ServeMux來簡化URL和handlers的聯(lián)系。一個ServeMux將一批http.Handler聚集到一個單一的http.Handler中。再一次,我們可以看到滿足同一接口的不同類型是可替換的:web服務器將請求指派給任意的http.Handler
而不需要考慮它后面的具體類型。
對于更復雜的應用,一些ServeMux可以通過組合來處理更加錯綜復雜的路由需求。Go語言目前沒有一個權威的web框架,就像Ruby語言有Rails和python有Django。這并不是說這樣的框架不存在,而是Go語言標準庫中的構建模塊就已經(jīng)非常靈活以至于這些框架都是不必要的。此外,盡管在一個項目早期使用框架是非常方便的,但是它們帶來額外的復雜度會使長期的維護更加困難。
在下面的程序中,我們創(chuàng)建一個ServeMux并且使用它將URL和相應處理/list和/price操作的handler聯(lián)系起來,這些操作邏輯都已經(jīng)被分到不同的方法中。然后我們在調(diào)用ListenAndServe函數(shù)中使用ServeMux為主要的handler。
gopl.io/ch7/http3
func main() {
db := database{"shoes": 50, "socks": 5}
mux := http.NewServeMux()
mux.Handle("/list", http.HandlerFunc(db.list))
mux.Handle("/price", http.HandlerFunc(db.price))
log.Fatal(http.ListenAndServe("localhost:8000", mux))
}
type database map[string]dollars
func (db database) list(w http.ResponseWriter, req *http.Request) {
for item, price := range db {
fmt.Fprintf(w, "%s: %s\n", item, price)
}
}
func (db database) price(w http.ResponseWriter, req *http.Request) {
item := req.URL.Query().Get("item")
price, ok := db[item]
if !ok {
w.WriteHeader(http.StatusNotFound) // 404
fmt.Fprintf(w, "no such item: %q\n", item)
return
}
fmt.Fprintf(w, "%s\n", price)
}
讓我們關注這兩個注冊到handlers上的調(diào)用。第一個db.list是一個方法值(§6.4),它是下面這個類型的值。
func(w http.ResponseWriter, req *http.Request)
也就是說db.list的調(diào)用會援引一個接收者是db的database.list方法。所以db.list是一個實現(xiàn)了handler類似行為的函數(shù),但是因為它沒有方法(理解:該方法沒有它自己的方法),所以它不滿足http.Handler接口并且不能直接傳給mux.Handle。
語句http.HandlerFunc(db.list)是一個轉(zhuǎn)換而非一個函數(shù)調(diào)用,因為http.HandlerFunc是一個類型。它有如下的定義:
net/http
package http
type HandlerFunc func(w ResponseWriter, r *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
HandlerFunc顯示了在Go語言接口機制中一些不同尋常的特點。這是一個實現(xiàn)了接口http.Handler的方法的函數(shù)類型。ServeHTTP方法的行為是調(diào)用了它的函數(shù)本身。因此HandlerFunc是一個讓函數(shù)值滿足一個接口的適配器,這里函數(shù)和這個接口僅有的方法有相同的函數(shù)簽名。實際上,這個技巧讓一個單一的類型例如database以多種方式滿足http.Handler接口:一種通過它的list方法,一種通過它的price方法等等。
因為handler通過這種方式注冊非常普遍,ServeMux有一個方便的HandleFunc方法,它幫我們簡化handler注冊代碼成這樣:
gopl.io/ch7/http3a
mux.HandleFunc("/list", db.list)
mux.HandleFunc("/price", db.price)
從上面的代碼很容易看出應該怎么構建一個程序:由兩個不同的web服務器監(jiān)聽不同的端口,并且定義不同的URL將它們指派到不同的handler。我們只要構建另外一個ServeMux并且再調(diào)用一次ListenAndServe(可能并行的)。但是在大多數(shù)程序中,一個web服務器就足夠了。此外,在一個應用程序的多個文件中定義HTTP handler也是非常典型的,如果它們必須全部都顯式地注冊到這個應用的ServeMux實例上會比較麻煩。
所以為了方便,net/http包提供了一個全局的ServeMux實例DefaultServerMux和包級別的http.Handle和http.HandleFunc函數(shù)?,F(xiàn)在,為了使用DefaultServeMux作為服務器的主handler,我們不需要將它傳給ListenAndServe函數(shù);nil值就可以工作。
然后服務器的主函數(shù)可以簡化成:
gopl.io/ch7/http4
func main() {
db := database{"shoes": 50, "socks": 5}
http.HandleFunc("/list", db.list)
http.HandleFunc("/price", db.price)
log.Fatal(http.ListenAndServe("localhost:8000", nil))
}
最后,一個重要的提示:就像我們在1.7節(jié)中提到的,web服務器在一個新的協(xié)程中調(diào)用每一個handler,所以當handler獲取其它協(xié)程或者這個handler本身的其它請求也可以訪問到變量時,一定要使用預防措施,比如鎖機制。我們后面的兩章中將講到并發(fā)相關的知識。
練習 7.11: 增加額外的handler讓客戶端可以創(chuàng)建,讀取,更新和刪除數(shù)據(jù)庫記錄。例如,一個形如 /update?item=socks&price=6
的請求會更新庫存清單里一個貨品的價格并且當這個貨品不存在或價格無效時返回一個錯誤值。(注意:這個修改會引入變量同時更新的問題)
練習 7.12: 修改/list的handler讓它把輸出打印成一個HTML的表格而不是文本。html/template包(§4.6)可能會對你有幫助。
![]() | ![]() |
更多建議: