gopl-zh.github.com/ch7/ch7-07.md
chai2010 9666211cd7 Fix typo
Fixes #198
2016-01-18 11:22:04 +08:00

9.5 KiB
Raw Blame History

7.7. http.Handler接口

在第一章中我們粗略的了解了怎麽用net/http包去實現網絡客戶端(§1.5)和服務器(§1.7)。在這個小節中我們會對那些基於http.Handler接口的服務器API做更進一步的學習

net/http
package http

type Handler interface {
	ServeHTTP(w ResponseWriter, r *Request)
}

func ListenAndServe(address string, h Handler) error

ListenAndServe函數需要一個例如“localhost:8000”的服務器地址和一個所有請求都可以分派的Handler接口實例。它會一直運行直到這個服務因爲一個錯誤而失敗或者啟動失敗它的返迴值一定是一個非空的錯誤。

想象一個電子商務網站爲了銷售它的數據庫將它物品的價格映射成美元。下面這個程序可能是能想到的最簡單的實現了。它將庫存清單模型化爲一個命名爲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節中的獲取程序如果你更喜歡可以使用web瀏覽器來連接服務器,我們得到下面的輸出:

$ go build gopl.io/ch1/fetch
$ ./fetch http://localhost:8000
shoes: $50.00
socks: $5.00

目前爲止這個服務器不考慮URL隻能爲每個請求列出它全部的庫存清單。更眞實的服務器會定義多個不同的URL每一個都會觸發一個不同的行爲。讓我們使用/list來調用已經存在的這個行爲併且增加另一個/price調用表明單個貨品的價格像這樣/price?item=socks來指定一個請求參數。

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

現在handler基於URL的路徑部分req.URL.Path來決定執行什麽邏輯。如果這個handler不能識别這個路徑它會通過調用w.WriteHeader(http.StatusNotFound)返迴客戶端一個HTTP錯誤這個檢査應該在向w寫入任何值前完成。順便提一下http.ResponseWriter是另一個接口。它在io.Writer上增加了發送HTTP相應頭的方法。等效地我們可以使用實用的http.Error函數

msg := fmt.Sprintf("no such page: %s\n", req.URL)
http.Error(w, msg, http.StatusNotFound) // 404

/price的case會調用URL的Query方法來將HTTP請求參數解析爲一個map或者更準確地説一個net/url包中url.Values(§6.2.1)類型的多重映射。然後找到第一個item參數併査找它的價格。如果這個貨品沒有找到會返迴一個錯誤。

這里是一個和新服務器會話的例子:

$ 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

顯然我們可以繼續向ServeHTTP方法中添加case但在一個實際的應用中將每個case中的邏輯定義到一個分開的方法或函數中會很實用。此外相近的URL可能需要相似的邏輯例如幾個圖片文件可能有形如/images/*.png的URL。因爲這些原因net/http包提供了一個請求多路器ServeMux來簡化URL和handlers的聯繫。一個ServeMux將一批http.Handler聚集到一個單一的http.Handler中。再一次我們可以看到滿足同一接口的不同類型是可替換的web服務器將請求指派給任意的http.Handler 而不需要考慮它後面的具體類型。

對於更複雜的應用一些ServeMux可以通過組合來處理更加錯綜複雜的路由需求。Go語言目前沒有一個權威的web框架就像Ruby語言有Rails和python有Django。這併不是説這樣的框架不存在而是Go語言標準庫中的構建模塊就已經非常靈活以至於這些框架都是不必要的。此外盡管在一個項目早期使用框架是非常方便的但是它們帶來額外的複雜度會使長期的維護更加睏難。

在下面的程序中我們創建一個ServeMux併且使用它將URL和相應處理/list和/price操作的handler聯繫起來這些操作邏輯都已經被分到不同的方法中。然後我門在調用ListenAndServe函數中使用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上的調用。第一個db.list是一個方法值 (§6.4),它是下面這個類型的值

func(w http.ResponseWriter, req *http.Request)

也就是説db.list的調用會援引一個接收者是db的database.list方法。所以db.list是一個實現了handler類似行爲的函數但是因爲它沒有方法所以它不滿足http.Handler接口併且不能直接傳給mux.Handle。

語句http.HandlerFunc(db.list)是一個轉換而非一個函數調用因爲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語言接口機製中一些不同尋常的特點。這是一個有實現了接口http.Handler方法的函數類型。ServeHTTP方法的行爲調用了它本身的函數。因此HandlerFunc是一個讓函數值滿足一個接口的適配器這里函數和這個接口僅有的方法有相同的函數籤名。實際上這個技巧讓一個單一的類型例如database以多種方式滿足http.Handler接口一種通過它的list方法一種通過它的price方法等等。

因爲handler通過這種方式註冊非常普遍ServeMux有一個方便的HandleFunc方法它幫我們簡化handler註冊代碼成這樣

gopl.io/ch7/http3a
mux.HandleFunc("/list", db.list)
mux.HandleFunc("/price", db.price)

從上面的代碼很容易看出應該怎麽構建一個程序它有兩個不同的web服務器監聽不同的端口的併且定義不同的URL將它們指派到不同的handler。我們隻要構建另外一個ServeMux併且在調用一次ListenAndServe可能併行的。但是在大多數程序中一個web服務器就足夠了。此外在一個應用程序的多個文件中定義HTTP handler也是非常典型的如果它們必須全部都顯示的註冊到這個應用的ServeMux實例上會比較麻煩。

所以爲了方便net/http包提供了一個全局的ServeMux實例DefaultServerMux和包級别的http.Handle和http.HandleFunc函數。現在爲了使用DefaultServeMux作爲服務器的主handler我們不需要將它傳給ListenAndServe函數nil值就可以工作。

然後服務器的主函數可以簡化成:

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節中提到的web服務器在一個新的協程中調用每一個handler所以當handler獲取其它協程或者這個handler本身的其它請求也可以訪問的變量時一定要使用預防措施比如鎖機製。我們後面的兩章中講到併發相關的知識。

練習 7.11 增加額外的handler讓客服端可以創建讀取更新和刪除數據庫記録。例如一個形如 /update?item=socks&price=6 的請求會更新庫存清單里一個貨品的價格併且當這個貨品不存在或價格無效時返迴一個錯誤值。(註意:這個脩改會引入變量同時更新的問題)

練習 7.12 脩改/list的handler讓它把輸出打印成一個HTML的表格而不是文本。html/template包(§4.6)可能會對你有幫助。