一、接口型函數
1.原始接口實現
type Handler interface { Do(k, v interface{}) } func Each(m map[interface{}]interface{}, h Handler) { if m != nil && len(m) > 0 { for k, v := range m { h.Do(k, v) } } }
這里具體要做什么,由實現Handler接口的類型自己去定義。也就是Each實現了面向接口編程。比如:
type welcome string func (w welcome) Do(k, v interface{}) { fmt.Printf("%s,我叫%s,今年%d歲\n", w,k, v) } func main() { persons := make(map[interface{}]interface{}) persons["張三"] = 20 persons["李四"] = 23 persons["王五"] = 26 var w welcome = "大家好" Each(persons, w) }
以上實現,我們定義了一個map來存儲學生們,map的key是學生的名字,value是該學生的年齡。welcome是我們新定義的類型,對應基本類型string,該welcome實現了Handler接口,打印出自我介紹。
2.接口型函數出場
以上實現,主要有兩點不太好:
- 因為必須要實現Handler接口,Do這個方法名不能修改,不能定義一個更有意義的名字
- 必須要新定義一個類型,才可以實現Handler接口,才能使用Each函數
首先我們先解決第一個問題,根據我們具體做的事情定義一個更有意義的方法名,比如例子中是自我介紹,那么使用selfInfo要比Do這個干巴巴的方法要好的多。
如果調用者改了方法名,那么就不能實現Handler接口,還要使用Each方法怎么辦?那就是由提供Each函數的負責提供Handler的實現,我們添加代碼如下:
type HandlerFunc func(k, v interface{}) func (f HandlerFunc) Do(k, v interface{}){ f(k,v) } type welcome string func (w welcome) selfInfo(k, v interface{}) { fmt.Printf("%s,我叫%s,今年%d歲\n", w,k, v) } func main() { persons := make(map[interface{}]interface{}) persons["張三"] = 20 persons["李四"] = 23 persons["王五"] = 26 var w welcome = "大家好" Each(persons, HandlerFunc(w.selfInfo)) }
還是差不多原來的實現,只是把方法名Do改為selfInfo。HandlerFunc(w.selfInfo)不是方法的調用,而是轉型,因為selfInfo和HandlerFunc是同一種類型,所以可以強制轉型。轉型后,因為HandlerFunc實現了Handler接口,所以我們就可以繼續使用原來的Each方法了。
3.進一步重構
現在解決了命名的問題,但是每次強制轉型不太好,我們繼續重構,可以采用新定義一個函數的方式,幫助調用者強制轉型。
func EachFunc(m map[interface{}]interface{}, f func(k, v interface{})) { Each(m,HandlerFunc(f)) } ... EachFunc(persons, w.selfInfo)
新增了一個EachFunc函數,幫助調用者強制轉型,調用者就不用自己做了。
現在我們發現EachFunc函數接收的是一個func(k, v interface{})類型的函數,沒有必要實現Handler接口了,所以我們新的類型可以去掉不用了。
func selfInfo(k, v interface{}) { fmt.Printf("大家好,我叫%s,今年%d歲\n", k, v) } func main() { persons := make(map[interface{}]interface{}) persons["張三"] = 20 persons["李四"] = 23 persons["王五"] = 26 EachFunc(persons, selfInfo) }
去掉了自定義類型welcome之后,整個代碼更簡潔,可讀性更好。我們的方法含義都是:
- 讓這學生自我介紹
- 讓這些學生起立
- 讓這些學生早讀
- 讓這些學生…
都是這種默認,方法處理,更符合自然語言規則。
4.總結
以上關於函數型接口就寫完了,如果我們仔細留意,發現和我們自己平時使用的http.Handle方法非常像,其實接口http.Handler就是這么實現的。
type Handler interface { ServeHTTP(ResponseWriter, *Request) } func Handle(pattern string, handler Handler) { DefaultServeMux.Handle(pattern, handler) } func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { DefaultServeMux.HandleFunc(pattern, handler) }
這是一種非常好的技巧,提供兩種函數,既可以以接口的方式使用,也可以以方法的方式,對應我們例子中的Each和EachFunc這兩個函數,靈活方便。
二、http.Handler接口
摘自《GO語言聖經》第7章
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並輸出物品信息。
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) } }
如果我們啟動這個服務,然后用web瀏覽器來連接localhost:8000,我們得到下面的輸出:
shoes: $50.00 socks: $5.00
目前為止,這個服務器不考慮URL只能為每個請求列出它全部的庫存清單。更真實的服務器會定義多個不同的URL,每一個都會觸發一個不同的行為。讓我們使用/list來調用已經存在的這個行為並且增加另一個/price調用表明單個貨品的價格,像這樣/price?item=socks來指定一個請求參數。
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
二、ServeMux
顯然我們可以繼續向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。
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是一個類型。它有如下的定義:
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方法等等。
這里原書說的有點繞,說一下個人的理解,先看一下使用方式
mux := http.NewServeMux() mux.Handle("/list", http.HandlerFunc(db.list)) ... func (mux *ServeMux) Handle(pattern string, handler Handler) {
可以看到Handle這個方法,要求傳入一個Handler接口類型,上文分析這個接口類型需要實現ServeHTTP(w ResponseWriter, r *Request)
即可。但是現在我們不想傳一個實現這種接口的類型,而是想傳入一個方法,並且這個方法干的事情和ServeHTTP一樣,連參數也一樣。這就像一個電源適配器一樣,只是改改插孔,這個適配器是這樣的:
package http type HandlerFunc func(w ResponseWriter, r *Request) func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) }
因為handler通過這種方式注冊非常普遍,ServeMux有一個方便的HandleFunc方法,它幫我們簡化handler注冊代碼成這樣:
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值就可以工作。然后服務器的主函數可以簡化成:
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本身的其它請求也可以訪問的變量時一定要使用預防措施比如鎖機制。
// Server2 is a minimal "echo" and counter server. package main import ( "fmt" "log" "net/http" "sync" ) var mu sync.Mutex var count int func main() { http.HandleFunc("/", handler) http.HandleFunc("/count", counter) log.Fatal(http.ListenAndServe("localhost:8000", nil)) } // handler echoes the Path component of the requested URL. func handler(w http.ResponseWriter, r *http.Request) { mu.Lock() count++ mu.Unlock() fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path) } // counter echoes the number of calls so far. func counter(w http.ResponseWriter, r *http.Request) { mu.Lock() fmt.Fprintf(w, "Count %d\n", count) mu.Unlock() }
這個服務器有兩個請求處理函數,根據請求的url不同會調用不同的函數:對/count這個url的請求會調用到count這個函數,其它的url都會調用默認的處理函數。如果你的請求pattern是以/結尾,那么所有以該url為前綴的url都會被這條規則匹配。在這些代碼的背后,服務器每一次接收請求處理時都會另起一個goroutine,這樣服務器就可以同一時間處理多個請求。然而在並發情況下,假如真的有兩個請求同一時刻去更新count,那么這個值可能並不會被正確地增加;這個程序可能會引發一個嚴重的bug:競態條件(參見9.1)。為了避免這個問題,我們必須保證每次修改變量的最多只能有一個goroutine,這也就是代碼里的mu.Lock()和mu.Unlock()調用將修改count的所有行為包在中間的目的。