gorilla/mux類庫解析


golang自帶的http.SeverMux路由實現簡單,本質是一個map[string]Handler,是請求路徑與該路徑對應的處理函數的映射關系。實現簡單功能也比較單一:

  1. 不支持正則路由, 這個是比較致命的
  2. 只支持路徑匹配,不支持按照Method,header,host等信息匹配,所以也就沒法實現RESTful架構

而gorilla/mux是一個強大的路由,小巧但是穩定高效,不僅可以支持正則路由還可以按照Method,header,host等信息匹配,可以從我們設定的路由表達式中提取出參數方便上層應用,而且完全兼容http.ServerMux

使用示例

r := mux.NewRouter()
   
//與http.ServerMux不同的是mux.Router是完全的正則匹配,設置路由路徑/index/,如果訪問路徑/idenx/hello會返回404
//設置路由路徑為/index/訪問路徑/index也是會報404的,需要設置r.StrictSlash(true), /index/與/index才能匹配
 r.HandleFunc("/index/", func(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("root path"))
})
    
//mux.Vars(r)會返回該請求所解析出的所有參數(map[string]string)
//訪問/hello/ghbai 會輸出 hello ghbai
 r.HandleFunc("/hello/{name:[a-zA-Z]+}", func(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte(fmt.Sprintf("hello %s", mux.Vars(r)["name"])))
})
    
http.Handle("/", r)

源碼實現

Router的實現

路由信息是存放在一個Route類型的數組([]Route)中,數組中的每一個Route對象都表示一條路由信息,其中包含匹配該路由應該滿足的所有條件及對應的上層處理Hanlder。當請求到來是Router會遍歷Route數組,找到第一個匹配的路由則執行對應的處理函數,如果找不到則執行NotFoundHandler。

type Router struct {
    routes []*Route
}
// Match matches registered routes against the request.
func (r *Router) Match(req *http.Request, match *RouteMatch) bool {
    for _, route := range r.routes {
        //Route.Match會檢查http.Request是否滿足其設定的各種條件(路徑,Header,Host..)
        if route.Match(req, match) {
            return true
        }
    }
    return false
}

func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    var match RouteMatch
    var handler http.Handler
    if r.Match(req, &match) {
        handler = match.Handler
    }
    if handler == nil {
        handler = http.NotFoundHandler()
    }
    handler.ServeHTTP(w, req)
}

Route的實現

Route的實現其實也比較簡單,正則表達式的解析不太好理解,subrouter的實現更是需要好好研究代碼,但是這些對理解Route的設計思路實現影響不大。每一個Route中包含一個matcher數組,是所有限定條件的集合,matcher是一個返回bool值的接口。

當我們添加路由限定條件時,就是往matcher數組中增加一個限定函數。 當請求到來時,Route.Match()會遍歷matcher數組,只有數組中所有的元素都返回true時則說明此請求滿足該路由的限定條件。

假設我們規定只能以GET方式訪問/user/{userid:[0-9]+}並且header中必須包含“Refer”:"example.com",才能得到我們想要的結果我們可以這樣設置路由

func userHandler(w http.ResponseWriter,r* http.Request) {
    w.write([]byte(fmt.Sprintf("user %s visited",mux.Vars(r)["userid"])))
}
r.HandleFunc("/user/{userid:[0-9]+}", userHandler)
.Methods("GET")
.Headers("Refer", "example.com")

然后我們來看下Route是如何保存這三個限定條件的

type Route struct {
     // Request handler for the route.
    handler http.Handler
    
    // List of matchers.
    matchers []matcher
}

//添加Header限定條件,請求的header中必須含有“Refer”,值為“example.com” 
func (r *Route) Headers(pairs ...string) *Route {
    if r.err == nil {
        var headers map[string]string
        //mapFromPairs返回一個map[string][string]{"Refer":"example.com"}
        headers, r.err = mapFromPairs(pairs...)
        return r.addMatcher(headerMatcher(headers))
    }
    return r
}

type headerMatcher map[string]string
func (m headerMatcher) Match(r *http.Request, match *RouteMatch) bool {
    //matchMap會判斷r.Header是否含有“Refer”,並且值為“example.com” 
    return matchMap(m, r.Header, true)
}

//methodMatcher就是取出r.Method然后判斷該方式是否是設定的Method
type methodMatcher []string
func (m methodMatcher) Match(r *http.Request, match *RouteMatch) bool {
    return matchInArray(m, r.Method)
}

func (r *Route) Methods(methods ...string) *Route {
    for k, v := range methods {
        methods[k] = strings.ToUpper(v)
    }
    return r.addMatcher(methodMatcher(methods))
}

//帶有正則表達式路徑匹配是比較復雜的 tpl就是/user/{userid:[0-9]+}
func (r *Route) Path(tpl string) *Route {
    r.err = r.addRegexpMatcher(tpl, false, false, false)
    return r
}

func (r *Route) addRegexpMatcher(tpl string,strictSlash bool) error {
    //braceIndices判斷{ }是否成對並且正確出現,idxs是'{' '}'在表達式tpl中的下標數組
    idxs, errBraces := braceIndices(tpl)
    
    template := tpl
    defaultPattern := "[^/]+"
    //保存所需要提取的所有變量名稱,此例是userid
    varsN := make([]string, len(idxs)/2)
    var end int //end 此時為0
    pattern := bytes.NewBufferString("")
    for i := 0; i < len(idxs); i += 2 {
        raw := tpl[end:idxs[i]] //raw="/user/"
        end = idxs[i+1]
        parts := strings.SplitN(tpl[idxs[i]+1:end-1], ":", 2) //parts=[]{"userid","[0-9]+"}
        name := parts[0]  //name="userid"
        patt := defaultPattern
        if len(parts) == 2 {
            patt = parts[1] //patt="[0-9]+"
        }
        //構造出最終的正則表達式 /usr/([0-9]+)
        fmt.Fprintf(pattern, "%s(%s)", regexp.QuoteMeta(raw), patt)
        varsN[i/2] = name //將所要提取的參數名userid保存到varsN中
    }//如果有其他正則表達式繼續遍歷
      raw := tpl[end:]
    pattern.WriteString(regexp.QuoteMeta(raw))
    if strictSlash {
        pattern.WriteString("[/]?")
    }
    //編譯最終的正則表達式
    reg, errCompile := regexp.Compile(pattern.String())
    
    rr = &routeRegexp{
        template:    template,
        regexp:      reg,
        varsN:       varsN,
    }
    r.addMatcher(rr)
}

func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool {
    return r.regexp.MatchString(getHost(req))
}

context上下文

上面三個限定條件是如何實現的已經分析完了,路徑匹配的最終正則表達式是/user/([0-9]+),參數名"userid"保存在varsN數組中,當正則匹配時提取出正則表達式中的參數值,並與varsN數組中的參數名稱做關聯,建立一個map[string][string]{"userid":"123456"}

var Vars map[string]string
pathVars := regexp.FindStringSubmatch(req.URL.Path)
if pathVars != nil {
    for k, v := range varsN {
        Vars[v] = pathVars[k+1]
    }
}

因為gorilla/mux選擇與http.ServerMux的接口保持一致,所以上層應用的處理函數也就變成了固定的 Hanlder

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

正則匹配解析出的參數Vars怎么傳遞給上層處理函數呢?gorilla/mux使用了一個第三方模塊gorilla/context。當http請求到來時,mux.Router會選擇合適的路由,並提取出一些參數信息,將這些參數信息與http.Request對象在gorilla/context中建立映射關系,上層處理函數根據http.Request對象到context中找到該http.Request所對應的參數信息。

context的實現如下

var data  = make(map[*http.Request]map[interface{}]interface{})
 
func Set(r *http.Request, key, val interface{}) {
    mutex.Lock()
    if data[r] == nil {
        data[r] = make(map[interface{}]interface{})
        datat[r] = time.Now().Unix()
    }
    data[r][key] = val
    mutex.Unlock()
}

func Get(r *http.Request, key interface{}) interface{} {
    mutex.RLock()
    if ctx := data[r]; ctx != nil {
        value := ctx[key]
        mutex.RUnlock()
        return value
    }
    mutex.RUnlock()
    return nil
}

上層處理函數中調用mux.Vars(r)則可以取出該http.Request所關聯的參數信息

//val實際上時一個map[string][string],存放該請求對應的變量值集合
func setVars(r *http.Request, val interface{}) {
    context.Set(r, varsKey, val)
}

func Vars(r *http.Request) map[string]string {
    if rv := context.Get(r, varsKey); rv != nil {
        //類型轉換,如果失敗直接panic
        return rv.(map[string]string)
    }
    return nil
}

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM