Golang構(gòu)建HTTP服務(二)--- Handler蜒茄,ServeMux與中間件

Golang標準庫http包提供了基礎的http服務落剪,這個服務又基于Handler接口和ServeMux結(jié)構(gòu)的做Mutilpexer。實際上场靴,go的作者設計Handler這樣的接口啡莉,不僅提供了默認的ServeMux對象,開發(fā)者也可以自定義ServeMux對象旨剥。

本質(zhì)上ServeMux只是一個路由管理器咧欣,而它本身也實現(xiàn)了Handler接口的ServeHTTP方法。因此圍繞Handler接口的方法ServeHTTP轨帜,可以輕松的寫出go中的中間件魄咕。

在go的http路由原理討論中,追本溯源還是討論Handler接口和ServeMux結(jié)構(gòu)蚌父。下面就基于這兩個對象開始更多關于go中http的故事吧哮兰。

介紹http庫源碼的時候,創(chuàng)建http服務的代碼很簡單苟弛,實際上代碼隱藏了很多細節(jié)喝滞,才有了后來的流程介紹。本文的目的主要是把這些細節(jié)暴露膏秫,從更底層的方式開始右遭,一步步隱藏細節(jié),完成樣例代碼的一樣的邏輯缤削。了解更多http包的原理之后窘哈,才能基于此構(gòu)建中間件。

自定義的Handler

標準庫http提供了Handler接口亭敢,用于開發(fā)者實現(xiàn)自己的handler宵距。只要實現(xiàn)接口的ServeHTTP方法即可。

關于約定名詞 handler函數(shù)吨拗,handler處理器满哪,handler,請參考http原理與源碼筆記中的定義劝篷。不然對下文的描述將會很困惑哨鸭。

type textHandler struct {
    responseText string
}

func (th *textHandler) ServeHTTP(w http.ResponseWriter, r *http.Request){
    fmt.Fprintf(w, th.responseText)
}

type indexHandler struct {}

func (ih *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "text/html")

    html := `<doctype html>
        <html>
        <head>
          <title>Hello World</title>
        </head>
        <body>
        <p>
          <a href="/welcome">Welcome</a> |  <a href="/message">Message</a>
        </p>
        </body>
</html>`
    fmt.Fprintln(w, html)
}

func main() {
    mux := http.NewServeMux()

    mux.Handle("/", &indexHandler{})

    thWelcome := &textHandler{"TextHandler !"}
    mux.Handle("/text",thWelcome)

    http.ListenAndServe(":8000", mux)
}

上面自定義了兩個handler結(jié)構(gòu),都實現(xiàn)了ServeHTTP方法娇妓。我們知道像鸡,NewServeMux可以創(chuàng)建一個ServeMux實例,ServeMux同時也實現(xiàn)了ServeHTTP方法,因此代碼中的mux也是一種handler只估。把它當成參數(shù)傳給http.ListenAndServe方法志群,后者會把mux傳給Server實例。因為指定了handler蛔钙,因此整個http服務就不再是DefaultServeMux锌云,而是mux,無論是在注冊路由還是提供請求服務的時候吁脱。

有一點值得注意桑涎,這里并沒有使用HandleFunc注冊路由,而是直接使用了mux注冊路由兼贡。當沒有指定mux的時候攻冷,系統(tǒng)需要創(chuàng)建一個默認的defaultServeMux,此時我們已經(jīng)有了mux遍希,因此不再需要http.HandleFucn方法了等曼,直接使用mux的Handle方法注冊即可。

此外凿蒜,Handle第二個參數(shù)是一個handler(處理器)禁谦,并不是HandleFunc的一個handler函數(shù),其原因也是因為mux.Handle本質(zhì)上就需要綁定url的pattern模式和handler(處理器)即可篙程。既然indexHandler是handle(處理器)枷畏,當然就能作為參數(shù),一切請求的處理過程虱饿,都交給器實現(xiàn)的接口方法ServeHTTP就行了拥诡。這個過程有點饒,如果不甚了解氮发,建議先閱讀http原理與源碼筆記了解注冊路由的本質(zhì)渴肉。下圖

handleFunc-handle.jpeg

左邊的12兩步只是為了創(chuàng)建一個ServeMux實例,然后調(diào)用實例的Handle方法爽冕,右邊的直接就調(diào)用了mux實例的Handle方法仇祭。

創(chuàng)建handler處理器

上面費勁口舌羅嗦,不就是1颈畸,2乌奇,3與3的差別么,并且1眯娱,2的兩步操作礁苗,封裝程度更高,開發(fā)者只需要寫函數(shù)即可徙缴,不用再定義結(jié)構(gòu)试伙。代碼更簡潔,因此,下面將直接創(chuàng)建handler函數(shù)疏叨,調(diào)用go的方法將函數(shù)轉(zhuǎn)變成handler(處理器)潘靖。

func text(w http.ResponseWriter, r *http.Request){
    fmt.Fprintln(w, "hello world")
}

func index(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "text/html")

    html := `<doctype html>
        <html>
        <head>
          <title>Hello World</title>
        </head>
        <body>
        <p>
          <a href="/welcome">Welcome</a> |  <a href="/message">Message</a>
        </p>
        </body>
</html>`
    fmt.Fprintln(w, html)
}

func main() {
    mux := http.NewServeMux()
    mux.Handle("/", http.HandlerFunc(index))
    mux.HandleFunc("/text", text)
    http.ListenAndServe(":8000", mux)
}

代碼中使用了http.HandlerFunc方法直接將一個handler函數(shù)轉(zhuǎn)變成實現(xiàn)了handler(處理器)。等價與圖中的3的步驟蚤蔓。

mux.HandleFunc("/text", text)就更進一步卦溢,與圖中的2步驟一致,與defaultServemux.HandleFunc(pattern, function)的用法一樣昌粤。

使用默認的DefaultServeMux

經(jīng)過了上面兩個過程的轉(zhuǎn)化既绕,隱藏了更多的細節(jié)啄刹,代碼與defaultServeMux的方式越來越像涮坐。下面再去掉自定義的ServeMux,只需要修改main函數(shù)的邏輯如下:

func main() {
    http.Handle("/", http.HandlerFunc(index))
    http.HandleFunc("/text", text)
    http.ListenAndServe(":8000", nil)
}

上述的代碼就和前文的例子一樣誓军,當代碼中不顯示的創(chuàng)建serveMux對象袱讹,http包就默認創(chuàng)建一個DefaultServeMux對象用來做路由管理器mutilplexer。

自定義Server

默認的DefaultServeMux創(chuàng)建的判斷來自server對象昵时,如果server對象不提供handler捷雕,才會使用默認的serveMux對象。既然ServeMux可以自定義壹甥,那么Server對象一樣可以救巷。

使用http.Server 即可創(chuàng)建自定義的server對象:

func main(){
    http.HandleFunc("/", index)

    server := &http.Server{
        Addr: ":8000",
        ReadTimeout: 60 * time.Second,
        WriteTimeout: 60 * time.Second,
    }
    server.ListenAndServe()
}

自定義的serverMux對象也可以傳到server對象中。

func main() {

    mux := http.NewServeMux()
    mux.HandleFunc("/", index)

    server := &http.Server{
        Addr: ":8000",
        ReadTimeout: 60 * time.Second,
        WriteTimeout: 60 * time.Second,
        Handler: mux,
    }
    server.ListenAndServe()
}

可見go中的路由和處理函數(shù)之間關系非常密切句柠,同時又很靈活浦译。通過巧妙的使用Handler接口,可以設計出優(yōu)雅的中間件程序溯职。

中間件Middleware

所謂中間件精盅,就是連接上下級不同功能的函數(shù)或者軟件,通常進行一些包裹函數(shù)的行為谜酒,為被包裹函數(shù)提供添加一些功能或行為叹俏。前文的HandleFunc就能把簽名為 func(w http.ResponseWriter, r *http.Reqeust)的函數(shù)包裹成handler。這個函數(shù)也算是中間件僻族。

這里我們以HTTP請求的中間件為例子粘驰,提供一個log中間件,能夠打印出每一個請求的log述么。

go的http中間件很簡單蝌数,只要實現(xiàn)一個函數(shù)簽名為func(http.Handler) http.Handler的函數(shù)即可。http.Handler是一個接口碉输,接口方法我們熟悉的為serveHTTP籽前。返回也是一個handler。因為go中的函數(shù)也可以當成變量傳遞或者或者返回,因此也可以在中間件函數(shù)中傳遞定義好的函數(shù)枝哄,只要這個函數(shù)是一個handler即可肄梨,即實現(xiàn)或者被handlerFunc包裹成為handler處理器。

func middlewareHandler(next http.Handler) http.Handler{
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request){
        // 執(zhí)行handler之前的邏輯
        next.ServeHTTP(w, r)
        // 執(zhí)行完畢handler后的邏輯
    })
}

這種方式在Elixir的Plug框架中很流行挠锥,思想偏向于函數(shù)式范式众羡。熟悉python的朋友一定也想到了裝飾器。閑話少說蓖租,來看看go是如何實現(xiàn)的吧:


func loggingHandler(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        log.Printf("Started %s %s", r.Method, r.URL.Path)
        next.ServeHTTP(w, r)
        log.Printf("Comleted %s in %v", r.URL.Path, time.Since(start))
    })
}

func main() {
    http.Handle("/", loggingHandler(http.HandlerFunc(index)))
    http.ListenAndServe(":8000", nil)
}

loggingHandler即是一個中間件函數(shù)粱侣,將請求的和完成的時間處理”突拢可以看見請求或go的輸出:

2016/12/04 21:18:13 Started GET /
2016/12/04 21:18:13 Comleted / in 13.365μs
2016/12/04 21:18:20 Started GET /
2016/12/04 21:18:20 Comleted / in 17.541μs

既然中間件是一種函數(shù)齐婴,并且簽名都是一樣,那么很容易就聯(lián)想到函數(shù)一層包一層的中間件稠茂。再添加一個函數(shù)柠偶,然后修改main函數(shù):


func hook(next http.Handler) http.Handler{
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        log.Println("before hook")
        next.ServeHTTP(w, r)
        log.Println("after hook")

    })
}

func main() {
    http.Handle("/", hook(loggingHandler(http.HandlerFunc(index))))
    http.ListenAndServe(":8000", nil)
}

在loggingHandler再包了一層hook,可以看到輸出為:

2016/12/04 21:26:30 before hook
2016/12/04 21:26:30 Started GET /
2016/12/04 21:26:30 Comleted / in 14.016μs
2016/12/04 21:26:30 after hook

函數(shù)調(diào)用形成了一條鏈睬关,可以是在這條鏈上做很多事情诱担。當然go的寫法上,比起elixir的|>的符號电爹,優(yōu)雅性略差蔫仙。

總結(jié)

通過對http包的源碼學習,我們了解了Handler接口和ServeMux結(jié)構(gòu)丐箩。并且知道如何配合他們實現(xiàn)go的中間件函數(shù)摇邦。當然,對于幾個約定名詞雏蛮,handler函數(shù)涎嚼,handler處理器和handler對象的理解,是掌握它們關系的關鍵因素挑秉,而handler處理器和handler對象的關系法梯,恰恰又是go接口使用的經(jīng)典例子,讓go具有一些動態(tài)類型的特性犀概。

了解了http服務如何構(gòu)建之后立哑,處理請求和返回響應就是下一個故事。而實現(xiàn)處理邏輯恰恰在我們一直在強調(diào)的ServeHTTP接口方法中姻灶。

接下來將會更詳細的討論請求和響應相關的函數(shù)對象铛绰。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蛔糯,一起剝皮案震驚了整個濱河市呈驶,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌想括,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,084評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件这嚣,死亡現(xiàn)場離奇詭異鸥昏,居然都是意外死亡,警方通過查閱死者的電腦和手機姐帚,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,623評論 3 392
  • 文/潘曉璐 我一進店門吏垮,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人罐旗,你說我怎么就攤上這事膳汪。” “怎么了九秀?”我有些...
    開封第一講書人閱讀 163,450評論 0 353
  • 文/不壞的土叔 我叫張陵遗嗽,是天一觀的道長。 經(jīng)常有香客問我颤霎,道長媳谁,這世上最難降的妖魔是什么涂滴? 我笑而不...
    開封第一講書人閱讀 58,322評論 1 293
  • 正文 為了忘掉前任友酱,我火速辦了婚禮,結(jié)果婚禮上柔纵,老公的妹妹穿的比我還像新娘缔杉。我一直安慰自己,他們只是感情好搁料,可當我...
    茶點故事閱讀 67,370評論 6 390
  • 文/花漫 我一把揭開白布或详。 她就那樣靜靜地躺著,像睡著了一般郭计。 火紅的嫁衣襯著肌膚如雪霸琴。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,274評論 1 300
  • 那天昭伸,我揣著相機與錄音梧乘,去河邊找鬼。 笑死庐杨,一個胖子當著我的面吹牛选调,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播灵份,決...
    沈念sama閱讀 40,126評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼仁堪,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了填渠?” 一聲冷哼從身側(cè)響起弦聂,我...
    開封第一講書人閱讀 38,980評論 0 275
  • 序言:老撾萬榮一對情侶失蹤鸟辅,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后莺葫,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體剔桨,經(jīng)...
    沈念sama閱讀 45,414評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,599評論 3 334
  • 正文 我和宋清朗相戀三年徙融,在試婚紗的時候發(fā)現(xiàn)自己被綠了洒缀。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,773評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡欺冀,死狀恐怖树绩,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情隐轩,我是刑警寧澤饺饭,帶...
    沈念sama閱讀 35,470評論 5 344
  • 正文 年R本政府宣布,位于F島的核電站职车,受9級特大地震影響瘫俊,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜悴灵,卻給世界環(huán)境...
    茶點故事閱讀 41,080評論 3 327
  • 文/蒙蒙 一扛芽、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧积瞒,春花似錦川尖、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,713評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至缰贝,卻和暖如春馍悟,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背剩晴。 一陣腳步聲響...
    開封第一講書人閱讀 32,852評論 1 269
  • 我被黑心中介騙來泰國打工锣咒, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人李破。 一個月前我還...
    沈念sama閱讀 47,865評論 2 370
  • 正文 我出身青樓宠哄,卻偏偏與公主長得像,于是被迫代替她去往敵國和親嗤攻。 傳聞我的和親對象是個殘疾皇子毛嫉,可洞房花燭夜當晚...
    茶點故事閱讀 44,689評論 2 354

推薦閱讀更多精彩內(nèi)容