十分鐘學會用Go編寫Web中間件

中間件(通常)是一小段代碼延刘,它們接受一個請求魂那,對其進行處理,每個中間件只處理一件事情锌蓄,完成后將其傳遞給另一個中間件或最終處理程序升筏,這樣就做到了程序的解耦。如果沒有中間件那么我們必須在最終的處理程序中來完成這些處理操作瘸爽,這無疑會造成處理程序的臃腫和代碼復用率不高的問題您访。中間件的一些常見用例是請求日志記錄,Header操縱剪决、HTTP請求認證和ResponseWriter劫持等等灵汪。


畫外音:上面這段描述中間件的文字,跟我兩年前在Laravel源碼解析之中間件寫的幾乎一樣(其實這圖也是從那里拿過來的)柑潦。再次說明做開發(fā)時間長了以后掌握一些編程的思想有時候比掌握一門編程語言更重要享言,這不咱們就又用Go來寫中間件了。

創(chuàng)建中間件

接下來我們用Go創(chuàng)建中間件渗鬼,中間件只將http.HandlerFunc作為其參數(shù)览露,在中間件里將其包裝并返回新的http.HandlerFunc供服務器服務復用器調用。這里我們創(chuàng)建一個新的類型Middleware譬胎,這會讓最后一起鏈式調用多個中間件變的更簡單差牛。

type Middleware func(http.HandlerFunc) http.HandlerFunc

下面的中間件通用代碼模板讓我們平時編寫中間件變得更容易。

中間件代碼模板

中間件是使用裝飾器模式實現(xiàn)的银择,下面的中間件通用代碼模板讓我們平時編寫中間件變得更容易多糠,我們在自己寫中間件的時候只需要往樣板里填充需要的代碼邏輯即可。

func createNewMiddleware() Middleware {

? ? // 創(chuàng)建一個新的中間件

? ? middleware := func(next http.HandlerFunc) http.HandlerFunc {

? ? ? ? // 創(chuàng)建一個新的handler包裹next

? ? ? ? handler := func(w http.ResponseWriter, r *http.Request) {

? ? ? ? ? ? // 中間件的處理邏輯

......

? ? ? ? ? ? // 調用下一個中間件或者最終的handler處理程序

? ? ? ? ? ? next(w, r)

? ? ? ? }

? ? ? ? // 返回新建的包裝handler

? ? ? ? return handler

? ? }

? ? // 返回新建的中間件

? ? return middleware

}

使用中間件

我們創(chuàng)建兩個中間件浩考,一個用于記錄程序執(zhí)行的時長夹孔,另外一個用于驗證請求用的是否是指定的HTTP Method,創(chuàng)建完后再用定義的Chain函數(shù)把http.HandlerFunc和應用在其上的中間件鏈起來,中間件會按添加順序依次執(zhí)行搭伤,最后執(zhí)行到處理函數(shù)只怎。完整的代碼如下:

package main

import (

? ? "fmt"

? ? "log"

? ? "net/http"

? ? "time"

)

type Middleware func(http.HandlerFunc) http.HandlerFunc

// 記錄每個URL請求的執(zhí)行時長

func Logging() Middleware {

? ? // 創(chuàng)建中間件

? ? return func(f http.HandlerFunc) http.HandlerFunc {

? ? ? ? // 創(chuàng)建一個新的handler包裝http.HandlerFunc

? ? ? ? return func(w http.ResponseWriter, r *http.Request) {

? ? ? ? ? ? // 中間件的處理邏輯

? ? ? ? ? ? start := time.Now()

? ? ? ? ? ? defer func() { log.Println(r.URL.Path, time.Since(start)) }()

? ? ? ? ? ? // 調用下一個中間件或者最終的handler處理程序

? ? ? ? ? ? f(w, r)

? ? ? ? }

? ? }

}

// 驗證請求用的是否是指定的HTTP Method,不是則返回 400 Bad Request

func Method(m string) Middleware {

? ? return func(f http.HandlerFunc) http.HandlerFunc {

? ? ? ? return func(w http.ResponseWriter, r *http.Request) {

? ? ? ? ? ? if r.Method != m {

? ? ? ? ? ? ? ? http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)

? ? ? ? ? ? ? ? return

? ? ? ? ? ? }

? ? ? ? ? ? f(w, r)

? ? ? ? }

? ? }

}

// 把應用到http.HandlerFunc處理器的中間件

// 按照先后順序和處理器本身鏈起來供http.HandleFunc調用

func Chain(f http.HandlerFunc, middlewares ...Middleware) http.HandlerFunc {

? ? for _, m := range middlewares {

? ? ? ? f = m(f)

? ? }

? ? return f

}

// 最終的處理請求的http.HandlerFunc

func Hello(w http.ResponseWriter, r *http.Request) {

? ? fmt.Fprintln(w, "hello world")

}

func main() {

? ? http.HandleFunc("/", Chain(Hello, Method("GET"), Logging()))

? ? http.ListenAndServe(":8080", nil)

}

運行程序后會打開瀏覽器訪問http://localhost:8080會有如下輸出:

2020/02/07 21:07:52 / 359.503μs

2020/02/07 21:09:17 / 34.727μs

到這里怎么用Go編寫和使用中間件就講完怜俐,也就十分鐘吧身堡。不過這里更多的是探究實現(xiàn)原理,那么在生產(chǎn)環(huán)境怎么自己使用編寫的這些中間件呢拍鲤,我們接著往下看贴谎。

使用gorilla/mux應用中間件

上面我們探討了如何創(chuàng)建中間件,但是使用上每次用Chain函數(shù)鏈接多個中間件和處理程序還是有些不方便季稳,而且在上一篇文章中我們已經(jīng)開始使用gorilla/mux提供的Router作為路由器了擅这。好在gorrila.mux支持向路由器添加中間件,如果發(fā)現(xiàn)匹配項景鼠,則按照添加中間件的順序執(zhí)行中間件仲翎,包括其子路由器也支持添加中間件。

gorrila.mux路由器使用Use方法為路由器添加中間件铛漓,Use方法的定義如下:

func (r *Router) Use(mwf ...MiddlewareFunc) {

for _, fn := range mwf {

r.middlewares = append(r.middlewares, fn)

}

}

它可以接受多個mux.MiddlewareFunc類型的參數(shù)溯香,mux.MiddlewareFunc的類型聲明為:

type MiddlewareFunc func(http.Handler) http.Handler

跟我們上面定義的Middleware類型很像也是一個函數(shù)類型,不過函數(shù)的參數(shù)和返回值都是http.Handler接口浓恶,在《深入學習用 Go 編寫 HTTP 服務器》中我們詳細講過http.Handler它 是net/http中定義的接口用來表示處理 HTTP 請求的對象玫坛,其對象必須實現(xiàn)ServeHTTP方法。我們把上面說的中間件模板稍微更改下就能創(chuàng)建符合gorrila.mux要求的中間件:

func CreateMuxMiddleware() mux.MiddlewareFunc {

// 創(chuàng)建中間件

return func(f http.Handler) http.Handler {

// 創(chuàng)建一個新的handler包裝http.HandlerFunc

return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

// 中間件的處理邏輯

......

// 調用下一個中間件或者最終的handler處理程序

f.ServeHTTP(w, r)

})

}

}

接下來问顷,我們把上面自定義的兩個中間件進行改造昂秃,然后應用到我們一直在使用的http_demo項目上禀梳,為了便于管理在項目中新建middleware目錄杜窄,兩個中間件分別放在log.go和http_method.go中

//middleware/log.go

func Logging() mux.MiddlewareFunc {

// 創(chuàng)建中間件

return func(f http.Handler) http.Handler {

// 創(chuàng)建一個新的handler包裝http.HandlerFunc

return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

// 中間件的處理邏輯

start := time.Now()

defer func() { log.Println(r.URL.Path, time.Since(start)) }()

// 調用下一個中間件或者最終的handler處理程序

f.ServeHTTP(w, r)

})

}

}

// middleware/http_demo.go

func Method(m string) mux.MiddlewareFunc {

return func(f http.Handler) http.Handler {

return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

if r.Method != m {

http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)

return

}

f.ServeHTTP(w, r)

})

}

}

然后在我們的路由器中進行引用:

func RegisterRoutes(r *mux.Router) {

r.Use(middleware.Logging())// 全局應用

indexRouter := r.PathPrefix("/index").Subrouter()

indexRouter.Handle("/", &handler.HelloHandler{})

userRouter := r.PathPrefix("/user").Subrouter()

userRouter.HandleFunc("/names/{name}/countries/{country}", handler.ShowVisitorInfo)

userRouter.Use(middleware.Method("GET"))//給子路由器應用

}

再次編譯啟動運行程序后訪問

http://localhost:8080/user/names/James/countries/NewZealand

從控制臺里可以看到,記錄了這個請求的處理時長:

2020/02/08 09:29:50 Starting HTTP server...

2020/02/08 09:55:20 /user/names/James/countries/NewZealan 51.157μs

到這里我們探究完了編寫Web中間件的過程和原理算途,在實際開發(fā)中只需要根據(jù)自己的需求按照我們給的中間件代碼模板編寫中間件即可塞耕,在編寫中間件的時候也要注意他們的職責范圍,不要所有邏輯都往里放嘴瓤。


前期我也和很多小伙伴一樣扫外,到處收集了很多資料,后面發(fā)現(xiàn)很多重復的廓脆!上面面都是自己整理好的筛谚!現(xiàn)在BAT夢想成真,我就把資料貢獻出來給有需要的人停忿!

順便求一波關注驾讲,哈哈~各位小伙伴關注我后私信【Java】就可以免費領取噠!

作者:kevinyan

原文鏈接:https://juejin.im/post/5e3e9884f265da57375c3138

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市吮铭,隨后出現(xiàn)的幾起案子时迫,更是在濱河造成了極大的恐慌,老刑警劉巖谓晌,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件掠拳,死亡現(xiàn)場離奇詭異,居然都是意外死亡纸肉,警方通過查閱死者的電腦和手機溺欧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來柏肪,“玉大人胧奔,你說我怎么就攤上這事≡み海” “怎么了龙填?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長拐叉。 經(jīng)常有香客問我岩遗,道長,這世上最難降的妖魔是什么凤瘦? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任宿礁,我火速辦了婚禮,結果婚禮上蔬芥,老公的妹妹穿的比我還像新娘梆靖。我一直安慰自己,他們只是感情好笔诵,可當我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布返吻。 她就那樣靜靜地躺著,像睡著了一般乎婿。 火紅的嫁衣襯著肌膚如雪测僵。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天谢翎,我揣著相機與錄音捍靠,去河邊找鬼。 笑死森逮,一個胖子當著我的面吹牛榨婆,可吹牛的內容都是我干的。 我是一名探鬼主播褒侧,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼良风,長吁一口氣:“原來是場噩夢啊……” “哼颜武!你這毒婦竟也來了?” 一聲冷哼從身側響起拖吼,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤鳞上,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后吊档,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體篙议,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年怠硼,在試婚紗的時候發(fā)現(xiàn)自己被綠了鬼贱。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡香璃,死狀恐怖这难,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情葡秒,我是刑警寧澤姻乓,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站眯牧,受9級特大地震影響蹋岩,放射性物質發(fā)生泄漏。R本人自食惡果不足惜学少,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一剪个、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧版确,春花似錦扣囊、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至忌堂,卻和暖如春盒至,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背士修。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留樱衷,地道東北人棋嘲。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像矩桂,于是被迫代替她去往敵國和親沸移。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,077評論 2 355

推薦閱讀更多精彩內容