一、接口型函數(shù)
1.原始接口實(shí)現(xiàn)
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)
}
}
}
這里具體要做什么蔼两,由實(shí)現(xiàn)Handler接口的類型自己去定義绅这。也就是Each實(shí)現(xiàn)了面向接口編程栗弟。比如:
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)
}
以上實(shí)現(xiàn),我們定義了一個map來存儲學(xué)生們规揪,map的key是學(xué)生的名字桥氏,value是該學(xué)生的年齡。welcome是我們新定義的類型猛铅,對應(yīng)基本類型string字支,該welcome實(shí)現(xiàn)了Handler接口,打印出自我介紹奸忽。
2.接口型函數(shù)出場
以上實(shí)現(xiàn)堕伪,主要有兩點(diǎn)不太好:
- 因?yàn)楸仨氁獙?shí)現(xiàn)Handler接口,Do這個方法名不能修改栗菜,不能定義一個更有意義的名字
- 必須要新定義一個類型欠雌,才可以實(shí)現(xiàn)Handler接口,才能使用Each函數(shù)
首先我們先解決第一個問題疙筹,根據(jù)我們具體做的事情定義一個更有意義的方法名富俄,比如例子中是自我介紹,那么使用selfInfo要比Do這個干巴巴的方法要好的多而咆。
如果調(diào)用者改了方法名霍比,那么就不能實(shí)現(xiàn)Handler接口,還要使用Each方法怎么辦暴备?那就是由提供Each函數(shù)的負(fù)責(zé)提供Handler的實(shí)現(xiàn)悠瞬,我們添加代碼如下:
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))
}
還是差不多原來的實(shí)現(xiàn),只是把方法名Do改為selfInfo涯捻。HandlerFunc(w.selfInfo)不是方法的調(diào)用浅妆,而是轉(zhuǎn)型,因?yàn)閟elfInfo和HandlerFunc是同一種類型汰瘫,所以可以強(qiáng)制轉(zhuǎn)型狂打。轉(zhuǎn)型后,因?yàn)镠andlerFunc實(shí)現(xiàn)了Handler接口混弥,所以我們就可以繼續(xù)使用原來的Each方法了趴乡。
3.進(jìn)一步重構(gòu)
現(xiàn)在解決了命名的問題对省,但是每次強(qiáng)制轉(zhuǎn)型不太好,我們繼續(xù)重構(gòu)晾捏,可以采用新定義一個函數(shù)的方式蒿涎,幫助調(diào)用者強(qiáng)制轉(zhuǎn)型。
func EachFunc(m map[interface{}]interface{}, f func(k, v interface{})) {
Each(m,HandlerFunc(f))
}
...
EachFunc(persons, w.selfInfo)
新增了一個EachFunc函數(shù)惦辛,幫助調(diào)用者強(qiáng)制轉(zhuǎn)型劳秋,調(diào)用者就不用自己做了。
現(xiàn)在我們發(fā)現(xiàn)EachFunc函數(shù)接收的是一個func(k, v interface{})類型的函數(shù)胖齐,沒有必要實(shí)現(xiàn)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之后呀伙,整個代碼更簡潔补履,可讀性更好。我們的方法含義都是:
- 讓這學(xué)生自我介紹
- 讓這些學(xué)生起立
- 讓這些學(xué)生早讀
- 讓這些學(xué)生…
都是這種默認(rèn)剿另,方法處理雏亚,更符合自然語言規(guī)則哲虾。
4.總結(jié)
以上關(guān)于函數(shù)型接口就寫完了逼侦,如果我們仔細(xì)留意曙痘,發(fā)現(xiàn)和我們自己平時使用的http.Handle方法非常像,其實(shí)接口http.Handler就是這么實(shí)現(xiàn)的氛堕。
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)
}
這是一種非常好的技巧馏臭,提供兩種函數(shù),既可以以接口的方式使用岔擂,也可以以方法的方式位喂,對應(yīng)我們例子中的Each和EachFunc這兩個函數(shù),靈活方便乱灵。
二塑崖、http.Handler接口
摘自《GO語言圣經(jīng)》第7章
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”的服務(wù)器地址,和一個所有請求都可以分派的Handler接口實(shí)例痛倚。它會一直運(yùn)行规婆,直到這個服務(wù)因?yàn)橐粋€錯誤而失敗(或者啟動失敳跷取)抒蚜,它的返回值一定是一個非空的錯誤。
想象一個電子商務(wù)網(wǎng)站耘戚,為了銷售它的數(shù)據(jù)庫將它物品的價格映射成美元嗡髓。下面這個程序可能是能想到的最簡單的實(shí)現(xiàn)了。它將庫存清單模型化為一個命名為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)
}
}
如果我們啟動這個服務(wù)长捧,然后用web瀏覽器來連接localhost:8000,我們得到下面的輸出:
shoes: $50.00
socks: $5.00
目前為止嚣鄙,這個服務(wù)器不考慮URL只能為每個請求列出它全部的庫存清單。更真實(shí)的服務(wù)器會定義多個不同的URL串结,每一個都會觸發(fā)一個不同的行為哑子。讓我們使用/list來調(diào)用已經(jīng)存在的這個行為并且增加另一個/price調(diào)用表明單個貨品的價格,像這樣/price?item=socks來指定一個請求參數(shù)肌割。
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錯誤声功;這個檢查應(yīng)該在向w寫入任何值前完成烦却。(順便提一下宠叼,http.ResponseWriter是另一個接口先巴。它在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會調(diào)用URL的Query方法來將HTTP請求參數(shù)解析為一個map伸蚯,或者更準(zhǔn)確地說一個net/url包中url.Values(§6.2.1)類型的多重映射。然后找到第一個item參數(shù)并查找它的價格简烤。如果這個貨品沒有找到會返回一個錯誤剂邮。這里是一個和新服務(wù)器會話的例子:
$ 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
顯然我們可以繼續(xù)向ServeHTTP方法中添加case横侦,但在一個實(shí)際的應(yīng)用中挥萌,將每個case中的邏輯定義到一個分開的方法或函數(shù)中會很實(shí)用。此外枉侧,相近的URL可能需要相似的邏輯引瀑;例如幾個圖片文件可能有形如/images/*.png的URL。因?yàn)檫@些原因榨馁,net/http包提供了一個請求多路器ServeMux來簡化URL和handlers的聯(lián)系憨栽。
一個ServeMux將一批http.Handler聚集到一個單一的http.Handler中。再一次翼虫,我們可以看到滿足同一接口的不同類型是可替換的:web服務(wù)器將請求指派給任意的http.Handler 而不需要考慮它后面的具體類型屑柔。對于更復(fù)雜的應(yīng)用,一些ServeMux可以通過組合來處理更加錯綜復(fù)雜的路由需求珍剑。
Go語言目前沒有一個權(quán)威的web框架掸宛,就像Ruby語言有Rails和python有Django。這并不是說這樣的框架不存在招拙,而是Go語言標(biāo)準(zhǔn)庫中的構(gòu)建模塊就已經(jīng)非常靈活以至于這些框架都是不必要的唧瘾。此外翔曲,盡管在一個項(xiàng)目早期使用框架是非常方便的,但是它們帶來額外的復(fù)雜度會使長期的維護(hù)更加困難劈愚。
在下面的程序中瞳遍,我們創(chuàng)建一個ServeMux并且使用它將URL和相應(yīng)處理/list和/price操作的handler聯(lián)系起來,這些操作邏輯都已經(jīng)被分到不同的方法中菌羽。然后我們在調(diào)用ListenAndServe函數(shù)中使用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)
}
讓我們關(guān)注這兩個注冊到handlers上的調(diào)用。
第一個db.list是一個方法值 (§6.4)注祖,它是下面這個類型的值
func(w http.ResponseWriter, req *http.Request)
也就是說db.list的調(diào)用會援引一個接收者是db的database.list方法猾蒂。所以db.list是一個實(shí)現(xiàn)了handler類似行為的函數(shù),但是因?yàn)樗鼪]有方法是晨,所以它不滿足http.Handler接口并且不能直接傳給mux.Handle肚菠。語句http.HandlerFunc(db.list)是一個轉(zhuǎn)換而非一個函數(shù)調(diào)用,因?yàn)閔ttp.HandlerFunc是一個類型罩缴。它有如下的定義:
package http
type HandlerFunc func(w ResponseWriter, r *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
HandlerFunc顯示了在Go語言接口機(jī)制中一些不同尋常的特點(diǎn)蚊逢。這是一個有實(shí)現(xiàn)了接口http.Handler方法的函數(shù)類型。ServeHTTP方法的行為調(diào)用了它本身的函數(shù)箫章。因此HandlerFunc是一個讓函數(shù)值滿足一個接口的適配器烙荷,這里函數(shù)和這個接口僅有的方法有相同的函數(shù)簽名。實(shí)際上檬寂,這個技巧讓一個單一的類型例如database以多種方式滿足http.Handler接口:一種通過它的list方法终抽,一種通過它的price方法等等。
這里原書說的有點(diǎn)繞桶至,說一下個人的理解昼伴,先看一下使用方式
mux := http.NewServeMux()
mux.Handle("/list", http.HandlerFunc(db.list))
...
func (mux *ServeMux) Handle(pattern string, handler Handler) {
可以看到Handle這個方法,要求傳入一個Handler接口類型镣屹,上文分析這個接口類型需要實(shí)現(xiàn)ServeHTTP(w ResponseWriter, r *Request)
即可圃郊。但是現(xiàn)在我們不想傳一個實(shí)現(xiàn)這種接口的類型,而是想傳入一個方法野瘦,并且這個方法干的事情和ServeHTTP一樣描沟,連參數(shù)也一樣。這就像一個電源適配器一樣鞭光,只是改改插孔,這個適配器是這樣的:
package http
type HandlerFunc func(w ResponseWriter, r *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
因?yàn)閔andler通過這種方式注冊非常普遍吏廉,ServeMux有一個方便的HandleFunc方法,它幫我們簡化handler注冊代碼成這樣:
mux.HandleFunc("/list", db.list)
mux.HandleFunc("/price", db.price)
從上面的代碼很容易看出應(yīng)該怎么構(gòu)建一個程序惰许,它有兩個不同的web服務(wù)器監(jiān)聽不同的端口的席覆,并且定義不同的URL將它們指派到不同的handler。我們只要構(gòu)建另外一個ServeMux并且在調(diào)用一次ListenAndServe(可能并行的)汹买。但是在大多數(shù)程序中佩伤,一個web服務(wù)器就足夠了聊倔。
此外,在一個應(yīng)用程序的多個文件中定義HTTP handler也是非常典型的生巡,如果它們必須全部都顯示的注冊到這個應(yīng)用的ServeMux實(shí)例上會比較麻煩耙蔑。所以為了方便,net/http包提供了一個全局的ServeMux實(shí)例DefaultServerMux和包級別的http.Handle和http.HandleFunc函數(shù)」氯伲現(xiàn)在甸陌,為了使用DefaultServeMux作為服務(wù)器的主handler,我們不需要將它傳給ListenAndServe函數(shù)盐股;nil值就可以工作钱豁。然后服務(wù)器的主函數(shù)可以簡化成:
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服務(wù)器在一個新的協(xié)程中調(diào)用每一個handler牲尺,所以當(dāng)handler獲取其它協(xié)程或者這個handler本身的其它請求也可以訪問的變量時一定要使用預(yù)防措施比如鎖機(jī)制。
// 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()
}
這個服務(wù)器有兩個請求處理函數(shù)幌蚊,根據(jù)請求的url不同會調(diào)用不同的函數(shù):對/count這個url的請求會調(diào)用到count這個函數(shù)谤碳,其它的url都會調(diào)用默認(rèn)的處理函數(shù)。如果你的請求pattern是以/結(jié)尾霹肝,那么所有以該url為前綴的url都會被這條規(guī)則匹配估蹄。在這些代碼的背后,服務(wù)器每一次接收請求處理時都會另起一個goroutine沫换,這樣服務(wù)器就可以同一時間處理多個請求。然而在并發(fā)情況下最铁,假如真的有兩個請求同一時刻去更新count讯赏,那么這個值可能并不會被正確地增加;這個程序可能會引發(fā)一個嚴(yán)重的bug:競態(tài)條件(參見9.1)冷尉。為了避免這個問題漱挎,我們必須保證每次修改變量的最多只能有一個goroutine,這也就是代碼里的mu.Lock()和mu.Unlock()調(diào)用將修改count的所有行為包在中間的目的雀哨。