Golang Gin 實戰(七)| 分組路由原始碼分析

echa攻城獅 發佈 2020-03-03T02:19:10+00:00

Go語言(golang)的一個很大的優勢,就是很容易的開發出網絡後台服務,而且性能快,效率高。在開發後端HTTP網絡應用服務的時候,我們需要處理很多HTTP的請求訪問,比如常見的API服務,我們就要處理很多HTTP請求,然後把處理的信息返回給使用者。



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.org\nwechat: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請求的時候,會發現很多不足,比如:

  1. 不能單獨的對請求方法(POST,GET等)註冊特定的處理函數
  2. 不支持Path變量參數
  3. 不能自動對Path進行校準
  4. 性能一般
  5. 擴展性不足
  6. ……

那麼如何解決以上問題呢?一個辦法就是自己寫一個處理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.org\nwechat: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.org\nwechat: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。

關鍵字: