Go語言經典庫使用分析(七)| 高性能可擴展 HTTP 路由 httprouter
Go語言(golang)的一個很大的優勢,就是很容易的開發出網絡後台服務,而且性能快,效率高。在開發後端HTTP網絡應用服務的時候,我們需要處理很多HTTP的請求訪問,比如常見的API服務,我們就要處理很多HTTP請求,然後把處理的信息返回給使用者。對於這類需求,Golang提供了內置的 net/http
包幫我們來處理這些HTTP請求,讓我們可以比較方便的開發一個HTTP服務。
net/http
func main() { http.HandleFunc("/",Index) log.Fatal(http.ListenAndServe(":8080", nil)) } func Index(w http.ResponseWriter, r *http.Request){ fmt.Fprint(w,"Blog:www.flysnow.orgnwechat:flysnow_org") }
這是 net/http
包中一個經典的HTTP服務實現,我們運行後打開 http://localhost:8080
,就可以看到如下信息:
Blog:www.flysnow.org wechat:flysnow_org
顯示的關鍵就是我們 http.HandleFunc
函數,我們通過該函數註冊了對路徑 /
的處理函數 Index
,所有才會看到上面的顯示信息。那麼這個 http.HandleFunc
他是如何註冊 Index
函數的呢?下面看看這個函數的源代碼。
// DefaultServeMux is the default ServeMux used by Serve. var DefaultServeMux = &defaultServeMux var defaultServeMux ServeMux func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { DefaultServeMux.HandleFunc(pattern, handler) } type ServeMux struct { mu sync.RWMutex m map[string]muxEntry hosts bool // whether any patterns contain hostnames }
看以上的源代碼,是存在一個默認的 DefaultServeMux
路由的,這個 DefaultServeMux
類型是 ServeMux
,我們註冊的路徑函數信息都被存入 ServeMux
的 m
字段中,以便處理HTTP請求的時候使用。
DefaultServeMux.HandleFunc
函數最終會調用 ServeMux.Handle
函數。
func (mux *ServeMux) Handle(pattern string, handler Handler) { //省略加鎖和判斷代碼 if mux.m == nil { mux.m = make(map[string]muxEntry) } //把我們註冊的路徑和相應的處理函數存入了m字段中 mux.m[pattern] = muxEntry{h: handler, pattern: pattern} if pattern[0] != '/' { mux.hosts = true } }
這下應該明白了,註冊的路徑和相應的處理函數都存入了m字段中。
既然註冊存入了相應的信息,那麼在處理HTTP請求的時候,就可以使用了。Go語言的 net/http
更底層細節就不詳細分析了,我們只要知道處理HTTP請求的時候,會調用 Handler
接口的 ServeHTTP
方法,而 ServeMux
正好實現了 Handler
。
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) { //省略一些無關代碼 h, _ := mux.Handler(r) h.ServeHTTP(w, r) }
上面代碼中的 mux.Handler
會獲取到我們註冊的 Index
函數,然後執行它,具體 mux.Handler
的詳細實現不再分析了,大家可以自己看下源代碼。
現在我們可以總結下 net/http
包對HTTP請求的處理。
HTTP請求->ServeHTTP函數->ServeMux的Handler方法->Index函數
這就是整個一條請求處理鏈,現在我們明白了 net/http
里對HTTP請求的原理。
net/http 的不足
我們自己在使用內置的 net/http
的默認路徑處理HTTP請求的時候,會發現很多不足,比如:
- 不能單獨的對請求方法(POST,GET等)註冊特定的處理函數
- 不支持Path變量參數
- 不能自動對Path進行校準
- 性能一般
- 擴展性不足
- ……
那麼如何解決以上問題呢?一個辦法就是自己寫一個處理HTTP請求的路由,因為從上面的源代碼我們知道, net/http
用的是默認的路徑。
//這個是我們啟動HTTP服務的函數,最後一個handler參數是nil http.ListenAndServe(":8080", nil) func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) { handler := sh.srv.Handler //這個判斷成立,因為我們傳遞的是nil if handler == nil { handler = DefaultServeMux } //省略了一些代碼 handler.ServeHTTP(rw, req) }
通過以上的代碼分析,我們自己在通過 http.ListenAndServe
函數啟動一個HTTP服務的時候,最後一個 handler
的值是nil,所以上面的nil判斷成立,使用的就是默認的路由 DefaultServeMux
。
現在我們就知道如何使用自己定義的路由了,那就是給 http.ListenAndServe
的最後一個參數 handler
傳一個自定義的路由,比如:
type CustomMux struct { } func (cm *CustomMux) ServeHTTP(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w,"Blog:www.flysnow.orgnwechat:flysnow_org") } func main() { log.Fatal(http.ListenAndServe(":8080", &CustomMux{})) }
這個自定義的 CustomMux
就是我們的路由,它顯示了和使用 net/http
演示的例子一樣的功能。
現在我們改變下代碼,只有 GET
方法才會顯示以上信息。
func (cm *CustomMux) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { fmt.Fprint(w,"Blog:www.flysnow.orgnwechat:flysnow_org") } else { fmt.Fprint(w,"bad http method request") } }
只需要改變下 ServeHTTP
方法的處理邏輯即可,現在我們可以換不同的請求方法試試,就會顯示不同的內容。
這個就是自定義,我們可以通過擴展 ServeHTTP
方法的實現來添加我們想要的任何功能,包括上面章節列出來的 net/http
的不足都可以解決,不過我們無需這麼麻煩,因為開源大牛已經幫我們做了這些事情,它就是 github.com/julienschmidt/httprouter
httprouter
httprouter 是一個高性能、可擴展的HTTP路由,上面我們列舉的 net/http
默認路由的不足,都被httprouter 實現,我們先用一個例子,認識下 httprouter 這個強大的 HTTP 路由。
package main import ( "fmt" "github.com/julienschmidt/httprouter" "net/http" "log" ) func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { fmt.Fprintf(w, "Blog:%s nWechat:%s","www.flysnow.org","flysnow_org") } func main() { router := httprouter.New() router.GET("/", Index) log.Fatal(http.ListenAndServe(":8080", router)) }
這個例子,實現了在 GET
請求 /
路徑時,會顯示如下信息:
Blog:www.flysnow.org wechat:flysnow_org
在這個例子中,首先通過 httprouter.New()
生成了一個 *Router
路由指針,然後使用 GET
方法註冊一個適配 /
路徑的 Index
函數,最後 *Router
作為參數傳給 ListenAndServe
函數啟動HTTP服務即可。
其實不止是 GET
方法,httprouter 為所有的HTTP Method 提供了快捷的使用方式,只需要調用對應的方法即可。
func (r *Router) GET(path string, handle Handle) { r.Handle("GET", path, handle) } func (r *Router) HEAD(path string, handle Handle) { r.Handle("HEAD", path, handle) } func (r *Router) OPTIONS(path string, handle Handle) { r.Handle("OPTIONS", path, handle) } func (r *Router) POST(path string, handle Handle) { r.Handle("POST", path, handle) } func (r *Router) PUT(path string, handle Handle) { r.Handle("PUT", path, handle) } func (r *Router) PATCH(path string, handle Handle) { r.Handle("PATCH", path, handle) } func (r *Router) DELETE(path string, handle Handle) { r.Handle("DELETE", path, handle) }
以上這些方法都是 httprouter 支持的,我們可以非常靈活的根據需要,使用對應的方法,這樣就解決了 net/http
默認路由的問題。
httprouter 命名參數
現代的API,基本上都是Restful API,httprouter提供的命名參數的支持,可以很方便的幫助我們開發Restful API。比如我們設計的API /user/flysnow
,這這樣一個URL,可以查看 flysnow
這個用戶的信息,如果要查看其他用戶的,比如 zhangsan
,我們只需要訪問API /user/zhangsan
即可。
現在我們可以發現,其實這是一種URL匹配模式,我們可以把它總結為 /user/:name
,這是一個通配符,看個例子。
func UserInfo(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { fmt.Fprintf(w, "hello, %s!n", ps.ByName("name")) } func main() { router := httprouter.New() router.GET("/user/:name",UserInfo) log.Fatal(http.ListenAndServe(":8080", router)) }
當我們運行,在瀏覽器里輸入 http://localhost:8080/user/flysnow
時,就會顯示 hello, flysnow!
.
通過上面的代碼示例,可以看到,路徑的參數是以 :
開頭的,後面緊跟着變量名,比如 :name
,然後在 UserInfo
這個處理函數中,通過 httprouter.Params
的 ByName
獲取對應的值。
:name
這種匹配模式,是精準匹配的,同時只能匹配一個,比如:
Pattern: /user/:name /user/gordon 匹配 /user/you 匹配 /user/gordon/profile 不匹配 /user/ 不匹配
因為httprouter這個路由就是單一匹配的,所以當我們使用命名參數的時候,一定要注意,是否有其他註冊的路由和命名參數的路由,匹配同一個路徑,比如 /user/new
這個路由和 /user/:name
就是衝突的,不能同時註冊。
這裡稍微提下httprouter的另外一種通配符模式,就是把 :
換成 *
,也就是 *name
,這是一種匹配所有的模式,不常用,比如:
Pattern: /user/*name /user/gordon 匹配 /user/you 匹配 /user/gordon/profile 匹配 /user/ 匹配
因為是匹配所有的 *
模式,所以只要 *
前面的路徑匹配,就是匹配的,不管路徑多長,有幾層,都匹配。
httprouter兼容http.Handler
通過上面的例子,我們應該已經發現, GET
方法的handle,並不是我們熟悉的 http.Handler
,它是httprouter自定義的,相比 http.Handler
多了一個通配符參數的支持。
type Handle func(http.ResponseWriter, *http.Request, Params)
自定義的Handle,唯一的目的就是支持通配符參數,如果你的HTTP服務里,有些路由沒有用到通配符參數,那麼可以使用原生的 http.Handler
,httprouter是兼容支持的,這也為我們從 net/http
的方式,升級為httprouter路由提供了方便,會高效很多。
func (r *Router) Handler(method, path string, handler http.Handler) { r.Handle(method, path, func(w http.ResponseWriter, req *http.Request, _ Params) { handler.ServeHTTP(w, req) }, ) } func (r *Router) HandlerFunc(method, path string, handler http.HandlerFunc) { r.Handler(method, path, handler) }
httprouter通過 Handler
和 HandlerFunc
兩個函數,提供了兼容 http.Handler
和 http.HandlerFunc
的完美支持。從以上源代碼中,我們可以看出,實現的方式也比較簡單,就是做了一個 http.Handler
到 httprouter.Handle
的轉換,捨棄了通配符參數的支持。
Handler處理鏈
得益於 http.Handler
的模式,我們可以把不同的 http.Handler
組成一個處理鏈, httprouter.Router
也是實現了 http.Handler
的,所以它也可以作為 http.Handler
處理鏈的一部分,比如和 Negroni
、 Gorilla handlers
這兩個庫配合使用,關於這兩個庫的介紹,可以參考我以前寫的文章。
Go語言經典庫使用分析(五)| Negroni 中間件(一) Go語言經典庫使用分析(三)| Gorilla Handlers 詳細介紹
這裡使用一個官方的例子,作為Handler處理鏈的演示。
比如對多個不同的二級域名,進行不同的路由處理。
//一個新類型,用於存儲域名對應的路由 type HostSwitch map[string]http.Handler //實現http.Handler接口,進行不同域名的路由分發 func (hs HostSwitch) ServeHTTP(w http.ResponseWriter, r *http.Request) { //根據域名獲取對應的Handler路由,然後調用處理(分發機制) if handler := hs[r.Host]; handler != nil { handler.ServeHTTP(w, r) } else { http.Error(w, "Forbidden", 403) } } func main() { //聲明兩個路由 playRouter := httprouter.New() playRouter.GET("/", PlayIndex) toolRouter := httprouter.New() toolRouter.GET("/", ToolIndex) //分別用於處理不同的二級域名 hs := make(HostSwitch) hs["play.flysnow.org:12345"] = playRouter hs["tool.flysnow.org:12345"] = toolRouter //HostSwitch實現了http.Handler,所以可以直接用 log.Fatal(http.ListenAndServe(":12345", hs)) }
以上就是一個簡單的,針對不同域名,使用不同路由的例子,代碼中的注釋比較詳細了,這裡就不一一解釋了。這個例子中, HostSwitch
和 httprouter.Router
這兩個 http.Handler
就組成了一個 http.Handler
處理鏈。
httprouter 靜態文件服務
httprouter提供了很方便的靜態文件服務,可以把一個目錄託管在服務器上,以供訪問。
router.ServeFiles("/static/*filepath",http.Dir("./"))
只需要這一句核心代碼即可,這個就是把當前目錄託管在服務器上,以供訪問,訪問路徑是 /static
。
使用 ServeFiles
需要注意的是,第一個參數路徑,必須要以 /*filepath
,因為要獲取我們要訪問的路徑信息。
func (r *Router) ServeFiles(path string, root http.FileSystem) { if len(path) < 10 || path[len(path)-10:] != "/*filepath" { panic("path must end with /*filepath in path '" + path + "'") } fileServer := http.FileServer(root) r.GET(path, func(w http.ResponseWriter, req *http.Request, ps Params) { req.URL.Path = ps.ByName("filepath") fileServer.ServeHTTP(w, req) }) }
這是源代碼實現,我們發現,最後還是一個 GET
請求服務,通過 http.FileServer
把 filepath
的路徑的內容顯示出來(如果路徑是個目錄則列出目錄文件;如果路徑是文件,則顯示內容)。
通過上面的源代碼,我們也可以知道, *filepath
這個通配符是為了獲取要放問的文件路徑,所以要符合預定,不然就會panic。
httprouter 異常捕獲
很少有路由支持這個功能的,httprouter允許使用者,設置 PanicHandler
用於處理HTTP請求中發生的panic。
func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { panic("故意拋出的異常") } func main() { router := httprouter.New() router.GET("/", Index) router.PanicHandler = func(w http.ResponseWriter, r *http.Request, v interface{}) { w.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(w, "error:%s",v) } log.Fatal(http.ListenAndServe(":8080", router)) }
演示例子中,我們通過設置 router.PanicHandler
來處理髮生的panic,處理辦法是打印出來異常信息。然後故意在 Index
函數中拋出一個painc,然後我們運行測試,會看到異常信息。
這是一種非常好的方式,可以讓我們對painc進行統一處理,不至於因為漏掉的panic影響用戶使用。
小結
httprouter還有不少有用的小功能,比如對404進行處理,我們通過設置 Router.NotFound
來實現,我們看看 Router
這個結構體的配置,可以發現更多有用的功能。
type Router struct { //是否通過重定向,給路徑自定加斜杠 RedirectTrailingSlash bool //是否通過重定向,自動修復路徑,比如雙斜杠等自動修復為單斜杠 RedirectFixedPath bool //是否檢測當前請求的方法被允許 HandleMethodNotAllowed bool //是否自定答覆OPTION請求 HandleOPTIONS bool //404默認處理 NotFound http.Handler //不被允許的方法默認處理 MethodNotAllowed http.Handler //異常統一處理 PanicHandler func(http.ResponseWriter, *http.Request, interface{}) }
這些字段都是導出的(export),我們可以直接設置,來達到我們的目的。
httprouter是一個高性能,低內存佔用的路由,它使用radix tree實現存儲和匹配查找,所以效率非常高,內存佔用也很低。關於radix tree大家可以查看相關的資料。
httprouter因為實現了 http.Handler
,所以可擴展性非常好,可以和其他庫、中間件結合使用,gin這個web框架就是使用的自定義的httprouter。
本文為原創文章,轉載註明出處,歡迎掃碼關注公眾號 flysnow_org
或者網站asf http://www.flysnow.org/ ,第一時間看後續精彩文章。覺得好的話,請猛擊文章右下角「好看」,感謝支持。
原文 : Go語言中文網