先简单实现一个 http 服务端,这个服务支持正则匹配路由处理:
package main import ( "fmt" "net/http" "regexp" ) // https://www.jianshu.com/p/4e8cdf3b2f88 // client -> Request -> Multiplexer(router)->handler ->Response -> client // 路由定义 type routeItem struct { pattern string // 正则表达式 f func(w http.ResponseWriter, r *http.Request) //Controller函数 } // 路由添加 var routePath = []routeItem{ {"^/user.*$", UserHandler}, {"^/company.*$", CompanyHandler}, } func UserHandler(w http.ResponseWriter, r *http.Request) { w.Write([]byte("hello user")) } func CompanyHandler(w http.ResponseWriter, r *http.Request) { w.Write([]byte("hello company")) } func IndexHandler(w http.ResponseWriter, r *http.Request) { isFound := false for _, p := range routePath { // 这里循环匹配Path,先添加的先匹配 reg, err := regexp.Compile(p.pattern) if err != nil { continue } if reg.MatchString(r.URL.Path) { isFound = true p.f(w, r) } } if !isFound { // 未匹配到路由 fmt.Fprint(w, "404 Page Not Found!") } } func main() { http.HandleFunc("/", IndexHandler) err := http.ListenAndServe("127.0.0.1:8899", nil) if err != nil { fmt.Println(err.Error()) return } }
上面的例子就得到一个结论:注册路由的方式可以是 HandleFunc ,其中直接将 / 匹配的请求全部交给 IndexHandler处理,在函数里面决定处理结果;
先是注册不同路由对应的handler,然后是 listen address port
开始阅读 go-zero的源代码:
无非两行:
// 注册路由 handler.RegisterHandlers(server, ctx) // 启动服务 server.Start()
最终启动服务 go-zero 框架用到的也是 *http.Server.ListenAndServe
其中 http.Server 对象是
&http.Server{ Addr: fmt.Sprintf("%s:%d", host, port), Handler: handler, }
注册的 handler 也就是路由的处理函数
github.com/zeromicro/go-zero/rest/engine.go 的 internal.StartHttp
传递的第三个参数 Router 是 http.Handler 的继承
所以很明显,整个 go-zero的web系统的路由注册在这里
那么,gozero究竟是否支持模糊匹配呢?是否支持正则匹配呢?
寻找的思路是:go的net/http最终都会转给
func(w http.ResponseWriter, r *http.Request)
的实现处理,gozero的模块划分比较清晰很容易定位到:
/github.com/zeromicro/go-zero/rest/router/patrouter.go -> ServeHTTP(w http.ResponseWriter, r *http.Request)
这个函数是每次请求的最终处理函数,那么它是否支持了正则匹配呢?
很明显,主要取决于工具包
func (t *Tree) Search(route string) (Result, bool)
Search的实现逻辑
然后是按照分隔符 '/' 严格匹配查找路径对应的handler的,所以不支持正则路由匹配
Search的代码
// Search searches item that associates with given route. func (t *Tree) Search(route string) (Result, bool) { if len(route) == 0 || route[0] != slash { return NotFound, false } var result Result ok := t.next(t.root, route[1:], &result) return result, ok } func (t *Tree) next(n *node, route string, result *Result) bool { log.Println("当前的存储的路径解析的参数结果集",result) log.Println("next route = ",route) log.Println("n.item = ",n.item) if len(route) == 0 && n.item != nil { result.Item = n.item return true } for i := range route { log.Println("i",i) if route[i] != slash { continue } token := route[:i] return n.forEach(func(k string, v *node) bool { fmt.Println("token=",token,"k",k) // 判定k的第一个字符是不是:,如果是将作为一个请求参数 // if pat[0] == colon { // colon是: r := match(k, token) // 截断已经读取过的 log.Println("last route is",route[i+1:]) if !r.found || !t.next(v, route[i+1:], result) { return false } log.Println("r.named",r.named) if r.named { addParam(result, r.key, r.value) } return true }) } }
写的有点冗余也挺绕的,就是循环每一个路径的字符,不断和常量 : / 比对,找到路径对应的 handler 的同时获取 :path 这种占位符的变量
/github.com/zeromicro/go-zero/core/search/tree.go
上面是比对的字符的两个常量;
路由匹配的难点在于它的route和handler的存储结构类似
{ school :{ handler, list:{ handler, get:{ * :{ } } } } }
它是一个以路由item( 路径 / 分割的字母 )为key的一个无限级递归查找 handler,
并且每次查找的时候都看看当前比如 /user/list/get/:id 看看当前的list有没有:开头,如果是:开头将认定为是参数变量添加到map参数集合之中
支持前缀路由匹配后的next 逻辑
const ( any = "*" )
func (t *Tree) next(n *node, route string, result *Result) bool { if len(route) == 0 && n.item != nil { result.Item = n.item return true } for i := range route { // 将当前的路径,递归查找逐个/的对应的handler if route[i] != slash { continue } token := route[:i] return n.forEach(func(k string, v *node) bool { if k == any { // any是字符串"*"直接使用当前*号所在的handler处理/支持前缀匹配 result.Item = v.item return true } r := match(k, token) if !r.found || !t.next(v, route[i+1:], result) { return false } if r.named { addParam(result, r.key, r.value) } return true }) } // 直到查找到最后的一个 / 后面的单词时候,将返回当前handler,将判定是否返回当前handler // 如果最后一个值不是 :name的这样的参数,但是在前面又没有找到handler - 将直接返回并且不给result.Item赋值 // 那么最后会被认为route not found return n.forEach(func(k string, v *node) bool { // 检查当前route是否是参数,如果是参数那么直接赋值并且返回当前handler if r := match(k, route); r.found && v.item != nil { result.Item = v.item if r.named { addParam(result, r.key, r.value) } return true } if k == any { // any是字符串"*"直接使用当前*号所在的handler处理/支持前缀匹配 result.Item = v.item return true } return false }) }
所以在 return false之前,判定一下当前的路由配置,如果是*那么直接赋 handler 处理,就可以实现模糊匹配
if k == "*" { // 直接使用当前*号所在的handler处理 result.Item = v.item return true }
答案如上;
还有一种使用方式,是覆盖 routeNotFound Hander
https://github.com/zeromicro/zero-examples/blob/main/http/wildcard/main.go
代码如下
func main() { flag.Parse() engine := rest.MustNewServer(rest.RestConf{ ServiceConf: service.ServiceConf{ Log: logx.LogConf{ Mode: "console", }, }, Host: "localhost", Port: *port, MaxConns: 500, }, rest.WithNotFoundHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/api/any/") { fmt.Fprintf(w, "wildcard: %s", r.URL.Path) } else { http.NotFound(w, r) } }))) defer engine.Stop() engine.AddRoute(rest.Route{ Method: http.MethodGet, Path: "/api/users", Handler: handle, }) engine.Start() }