基本信息
- 介绍:golang的http router
- 看这个源码的原因:gorilla的组织下有一些web常用的基础组件,印象中代码质量还不错,被很多项目使用。先看个比较简单的router,职责单一。
- github: https://github.com/gorilla/mux
官方http库的不足
golang官方的router,一般推荐用法是GET+url param(GET /articles?id=1)或者POST+data(POST /articles)。但是常见的比如REST里的嵌套资源,
比如GET /categories/1/articles/11,官方的库就支持不太好。
mux的亮点主要是:
- 兼容官方的
http.ServeMux,无痛迁移 - 相比官方的只能完全匹配或者找最长接近匹配,支持更多类型的匹配:host,path,path prefix,schemes, header and query values等;
- hosts, paths and query可以使用正则并提取变量
- 基于注册的路由反向构建url
- 嵌套路由支持
原理
主要讲讲它如何实现标准库没有事情。
更丰富的路由匹配规则
因为官方说明是支持标准库的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