golang自带的http.SeverMux路由实现简单,本质是一个map[string]Handler,是请求路径与该路径对应的处理函数的映射关系。实现简单功能也比较单一:
- 不支持正则路由, 这个是比较致命的
- 只支持路径匹配,不支持按照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 }