mux

基本信息

官方http库的不足

golang官方的router,一般推荐用法是GET+url param(GET /articles?id=1)或者POST+data(POST /articles)。但是常见的比如REST里的嵌套资源,
比如GET /categories/1/articles/11,官方的库就支持不太好。

mux的亮点主要是:

  1. 兼容官方的http.ServeMux,无痛迁移
  2. 相比官方的只能完全匹配或者找最长接近匹配,支持更多类型的匹配:host,path,path prefix,schemes, header and query values等;
  3. hosts, paths and query可以使用正则并提取变量
  4. 基于注册的路由反向构建url
  5. 嵌套路由支持

原理

主要讲讲它如何实现标准库没有事情。

更丰富的路由匹配规则

因为官方说明是支持标准库的http.ServeMux,所以我们看它怎么实现http.Handler接口即可。
逻辑见Routr.ServeHTTP

// ServeHTTP dispatches the handler registered in the matched route.
//
// When there is a match, the route variables can be retrieved calling
// mux.Vars(request).
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
        req = requestWithVars(req, match.Vars)
        req = requestWithRoute(req, match.Route)
    }

    if handler == nil && match.MatchErr == ErrMethodMismatch {
        handler = methodNotAllowedHandler()
    }

    if handler == nil {
        handler = http.NotFoundHandler()
    }

    handler.ServeHTTP(w, req)

我们可以查看,主要的逻辑就是调用Router.Match方法,得到匹配的路由,handler,变量和错误信息。
Router.Match的主要逻辑是:

func (r *Router) Match(req *http.Request, match *RouteMatch) bool {
    for _, route := range r.routes {
        if route.Match(req, match) {
            // Build middleware chain if no error was found
            if match.MatchErr == nil {
                for i := len(r.middlewares) - 1; i >= 0; i-- {
                    match.Handler = r.middlewares[i].Middleware(match.Handler)
                }
            }
            return true
        }
    }
    ...
}

可以看到主要内容是遍历注册的路由查找匹配项。
Route.Match做的事是:

func (r *Route) Match(req *http.Request, match *RouteMatch) bool {
    ...
    for _, m := range r.matchers {
        if matched := m.Match(req, match); !matched {
            ...
        }
    }
    ...
    // if matched
    ...
}

可看到是遍历routeConf.matchers匹配请求。routeConf.matchers是我们在使用Host,Path, PathPrefix等注册路由
时候,根据给定的规则添加到routeConf.matchers的。

说明了routeConf.matchers,我们回过来讲matcher.Match

type matcher interface {
    Match(*http.Request, *RouteMatch) bool
}

可以看到它是一个interface定义,正好和它的生成相对应的,每个路由注册时候使用的匹配项,都有一个对应实现,比如简单的headerMatcher实现:

// headerMatcher matches the request against header values.
type headerMatcher map[string]string

func (m headerMatcher) Match(r *http.Request, match *RouteMatch) bool {
    return matchMapWithString(m, r.Header, true)
}

所以我们回过头来看,mux丰富的匹配规则,其实是底层实现了各自对应的匹配逻辑。Router has many Routes, Route has many matchers.

变量抽取

Route.Match里,
当我们有匹配的matcher,最后一步便是抽取变量。

func (r *Route) Match(req *http.Request, match *RouteMatch) bool {
    ...
    // has a match...
    // Set variables.
    r.regexp.setMatch(req, match, r)
    return true
}

routeRegexpGroup.setMatch
的逻辑是:

func (v routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) {
    // Store host variables.
    ...
    // Store path variables.
    ...
    // Store query string variables.
        for _, q := range v.queries {
        queryURL := q.getURLQuery(req)
        matches := q.regexp.FindStringSubmatchIndex(queryURL)
        if len(matches) > 0 {
            extractVars(queryURL, matches, q.varsN, m.Vars)
        }
    }
}

可以看到会尝试抽取host,path,query变量,基本的逻辑都是正则匹配到匹配项,然后抽取出来赋值给之前抽取出来的变量。
具体抽取变量逻辑见newRouteRegexp

url构建

主要逻辑在(Route.URL)[https://sourcegraph.com/github.com/gorilla/[email protected]/-/blob/route.go?L533:17-533:20],
可以看到是先把传进来的数组两两匹配变成map,然后一次填充host,path,query,返回url.URL:

func (r *Route) URL(pairs ...string) (*url.URL, error) {
    ...
    values, err := r.prepareVars(pairs...)  // []string两两匹配变成map[string]string
    ...
    // host, path, query match...use path as example...
    if r.regexp.path != nil {
        if path, err = r.regexp.path.url(values); err != nil {
            return nil, err
        }
    }
    ...
    return &url.URL{
            Scheme:   scheme,
            Host:     host,
            Path:     path,
            RawQuery: strings.Join(queries, "&"),
    }, nil
}

routeRegexp.url的逻辑:

func (r *routeRegexp) url(values map[string]string) (string, error) {
    urlValues := make([]interface{}, len(r.varsN), len(r.varsN))
    for k, v := range r.varsN {
        value, ok := values[v]
        if !ok {
            return "", fmt.Errorf("mux: missing route variable %q", v)
        }
        if r.regexpType == regexpTypeQuery {
            value = url.QueryEscape(value)
        }
        urlValues[k] = value
    }
    rv := fmt.Sprintf(r.reverse, urlValues...)
    if !r.regexp.MatchString(rv) {
        // The URL is checked against the full regexp, instead of checking
        // individual variables. This is faster but to provide a good error
        // message, we check individual regexps if the URL doesn't match.
        for k, v := range r.varsN {
            if !r.varsR[k].MatchString(values[v]) {
                return "", fmt.Errorf(
                    "mux: variable %q doesn't match, expected %q", values[v],
                    r.varsR[k].String())
            }
        }
    }
    return rv, nil
}

嵌套路由

Route.SubRouter的原理很简单,
在你调用这个函数时,新建一个Route,会拷贝父对象的配置和命名路由,然后把自己加到父对象的matchers里,并返回自己。

func (r *Route) Subrouter() *Router {
    // initialize a subrouter with a copy of the parent route's configuration
    router := &Router{routeConf: copyRouteConf(r.routeConf), namedRoutes: r.namedRoutes}
    r.addMatcher(router)
    return router
}

当进行路由匹配时,
parent route会调用child route的Match方法,相对于是递归调用来匹配。

func (r *Route) Match(req *http.Request, match *RouteMatch) bool {
    ...
    for _, m := range r.matchers {
        if matched := m.Match(req, match); !matched {
            ...
        }
        ...
    }
    ...
}

middleware

mux middleware的工作原理是,在调用Router.Use时,会往Router.middlewares切片里append。

func (r *Router) Use(mwf ...MiddlewareFunc) {
    for _, fn := range mwf {
        r.middlewares = append(r.middlewares, fn)
    }
}

匹配到路由后,会从后往前依次执行:

func (r *Router) Match(req *http.Request, match *RouteMatch) bool {
    for _, route := range r.routes {
        if route.Match(req, match) {
            // Build middleware chain if no error was found
            if match.MatchErr == nil {
                for i := len(r.middlewares) - 1; i >= 0; i-- {
                    match.Handler = r.middlewares[i].Middleware(match.Handler)
                }
            }
            return true
        }
    }
    ...
}

不错的学习点

todo