Golang HTTP路由调研

路由是每个Web框架非常重要的一环, 这里我们调研一下几个Go路由的实现.

http.ServeMux

http.ServeMux 是标准库自带的URL路由. 其实现比较简单, 每个路径注册到一个字典里面, 查找的时候, 遍历字典, 并匹配最长路径.

package http

// 回调函数接口
type HandlerFunc func(ResponseWriter, *Request)


// 路由结构
type ServeMux struct {
	mu    sync.RWMutex
	m     map[string]muxEntry // 路由查找字典
	hosts bool // whether any patterns contain hostnames
}

// 注册路由
func (mux *ServeMux) Handle(pattern string, handler Handler)

// 路由查找逻辑
func (mux *ServeMux) match(path string) (h Handler, pattern string) {
	var n = 0
	for k, v := range mux.m {
		if !pathMatch(k, path) {
			continue
		}
		if h == nil || len(k) > n {
			n = len(k)
			h = v.h
			pattern = v.pattern
		}
	}
	return
}

func pathMatch(pattern, path string) bool {
	if len(pattern) == 0 {
		// should not happen
		return false
	}
	n := len(pattern)
	if pattern[n-1] != '/' {
		return pattern == path
	}
	return len(path) >= n && path[0:n] == pattern
}

http.ServeMux的局限性:

httprouter

目前项目使用的是gin, 其路由采用了httprouter. 相对于 http.ServeMux, httprouter支持:

我们来看下其路由解析实现:

package httprouter

// 回调函数接口, 提供了Params参数
type Handle func(http.ResponseWriter, *http.Request, Params)

// 注册路由, 多了method参数
func (r *Router) Handle(method, path string, handle Handle)

// 路由结构
type Router struct {
	trees map[string]*node // 路由查找树
    ...
}

// 节点结构
type node struct {
    path      string
    wildChild bool
    nType     nodeType
    maxParams uint8
    indices   string
    children  []*node
    handle    Handle
    priority  uint32
}

// 路由查找逻辑
func (n *node) getValue(path string) (handle Handle, p Params, tsr bool) {
walk: // outer loop for walking the tree
    for {
        if len(path) > len(n.path) {
            if path[:len(n.path)] == n.path {
                path = path[len(n.path):]
                ...
                c := path[0]
                for i := 0; i < len(n.indices); i++ {
                    if c == n.indices[i] {
                        n = n.children[i]
                        continue walk
                    }
                }
                ...
        } else if path == n.path {
            // We should have reached the node containing the handle.
            // Check if this node has a handle registered.
            if handle = n.handle; handle != nil {
                return
            }
            ...
        }
    }
    ...
}

从路由实现上来看, 用了 radix tree 的结构, 查找的时候更加高效; 在遇到匹配的时候立即返回, 不像http.ServeMux需要遍历决议.

问题

由于其精确路由, 因此没法把部分路由功能分发到另外一个路由器中. 也就是说, 一旦上了车, 就下不来了.

例如 net/http/pprof.Index 自己实现了子路经的派发功能, 就很难嵌入到 httprouter 中去.

同一路径下不支持固定路径和参数路径共存, 例

r.GET("/list", listHandler)
r.GET("/:method", dispatchHandler)
// runtime panic

参见这里.

虽然这是特性而不是BUG, 但是使用过程中确有不爽.

gin

gin 的路由器是基于 httprouter 的. 提几个比较有用的功能:

看个例子

r := gin.New()
g := r.Group("/user")
// 每个路由组可以共用中间层
g.Use(ThrottleHanler)
// 注册api相关方法
g.GET("/list", ...)
// ...

beego

beego是国人开发的Web开发框架, 在go-http-routing-benchmark中, 其路由性能似乎表现不佳, 我们来深究下原因.

看一下其路由实现, 也是使用了查找树, 但是对子节点查找时, 需要遍历, 而httprouter的每个节点, 保存了对于其子节点的路由信息node.indices, 查找上自然更快. 此外, beego路由查找方法使用了递归的方式(Tree.match), 而httprouter在一个执行循环(node.getValue)里就可以搞定, 自然效率更高.

// 路由结构
type ControllerRegister struct {
	routers      map[string]*Tree // 路由查找树
	...
}

// 节点结构
// Tree has three elements: FixRouter/wildcard/leaves
// fixRouter sotres Fixed Router
// wildcard stores params
// leaves store the endpoint information
type Tree struct {
	//prefix set for static router
	prefix string
	//search fix route first
	fixrouters []*Tree
	//if set, failure to match fixrouters search then search wildcard
	wildcard *Tree
	//if set, failure to match wildcard search
	leaves []*leafInfo
}

// 路由查找逻辑
func (t *Tree) match(pattern string, wildcardValues []string, ctx *context.Context) (runObject interface{}) {
    ...
    for _, subTree := range t.fixrouters {
        if subTree.prefix == seg {
            runObject = subTree.match(pattern, wildcardValues, ctx)
            if runObject != nil {
                break
            }
        }
    }
    ...
}

当然, 拿httprouter一个纯路由库, 和beego这样一个功能丰富的MVC开发框架, 是不公平的.

beego路由模块和Controller联系紧密, 提供了更加丰富的功能, 如大小写识别, 路由过滤器等. 列两点看上去挺有用的特性:

总结

对于路由选择, 够用就好, http.ServeMux 从功能以及性能上都不够令人满意, 建议用 httprouter 替代. 如果做后端API服务, gin挺趁手, 功能基本够用了. beego 的路由, 是为了其MVC框架服务的, 不方便单独拿出来用.

参考连接

HOME