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