为什么总是传入 Write、Router和 Param是什么?

func CreateUser(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
	io.WriteString(w, "Create User Handler")
}

这三个参数是用于处理HTTP请求和响应的。在Web应用程序中,HTTP请求通常包含有关客户端(例如浏览器)正在请求的资源以及其他相关信息。HTTP响应是服务器返回给客户端的内容,包括HTML,JSON,XML或其他类型的数据。

具体来说,这三个参数的含义如下:

  1. w http.ResponseWriter 用于向客户端发送HTTP响应。您可以使用它来设置响应头、状态码和响应正文。
  2. r *http.Request 包含客户端的HTTP请求信息,包括请求方法、请求头、请求体、URL参数等等。您可以使用它来获取客户端发送的数据或信息。
  3. p httprouter.Params 包含从URL路径中提取的参数。在路由处理程序中,您可以使用这些参数来执行特定的操作,例如从数据库中检索数据或调用其他API。

因此,这三个参数是处理HTTP请求和响应所必需的。在这个示例中,函数 CreateUser 可能是一个HTTP路由处理程序,通过这些参数来接收HTTP请求并生成HTTP响应。

简介

官方:https://github.com/julienschmidt/httprouter

文档:https://godoc.org/github.com/julienschmidt/httprouter

HttpRouter is a lightweight high performance HTTP request router (also called multiplexer or just mux for short) for Go. In contrast to the default mux of Go’s net/http package, this router supports variables in the routing pattern and matches against the request method. It also scales better. The router is optimized for high performance and a small memory footprint. It scales well even with very long paths and a large number of routes. A compressing dynamic trie (radix tree) structure is used for efficient matching.

简单描述,httprouter是一个golang实现的路由组件。httprouter使用一个前缀树来维护映射的父子关系,通过前缀树快速路由。同时其里面的HttpRouter结构体实现了golang的net.http.server的Handler接口,可以作为httpHandle发布。golang以性能出名的gin使用的也就是httprouter来做路由处理。

在go web开发中,很多时候会选用一款web框架,随着项目功能增加,接口也越来越多,一款好的路由框架能很方便的帮助我们管理与维护繁多的接口地址。

特性

  • 基于基数树实现的高性能路由框架
  • 仅支持精确匹配
  • 不必关心 URL 结尾的斜线
  • 路径自动校正,例如在 url 路径当中有../,//的时候
  • 可以在 URL 当中设置参数,例如/user/:id
  • 零内存分配
  • 不存在服务器崩溃,可以通过设置panic handler使服务器从 panic 当中恢复
  • 适合 API 构建

依赖

require github.com/julienschmidt/httprouter latest

使用Demo

package httpRouterDemo

import (
    "net/http"
    "github.com/julienschmidt/httprouter"
)

func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
    w.Write([]byte("hello world"))
}

func main() {
    router := httprouter.New()
    router.GET("/", Index)
    http.ListenAndServe(":80", router)
}

源码分析

解决两个问题,就基本明白了这个路由框架

  • 路由是是如何注册?如何保存的?
  • 当请求到来之后,路由是如何匹配,如何查找的?

还是从一个Hello World讲起

func main()  {
	r := httprouter.New()
	r.GET("/:name", func(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
        fmt.Fprintf(writer, "hello, %s!\n", params.ByName("name"))
	})
	http.ListenAndServe(":8080",r)
}

httprouter.New()初始化了一个 Router,下面直接看一下 Router 的结构

Router

在 Router 的源码当中有十分详尽的注释,这里按照我个人的理解注释一下

// Router实现了Http.Handler接口,用于注册分发路由
type Router struct {
    // trees 是一个基数树集合,每一个HTTP方法对应一棵单独的路由树
    // node是基数树的根节点
	trees map[string]*node

    // 用于开启上文提到的自动处理URL尾部斜杆的特性
    // 这个值为true时,如果/foo/没有被匹配到,会尝试匹配/foo
	RedirectTrailingSlash bool

	// 用于开启上文提到的路由校正的特性
    // 这个值为true时,会对../和//这种路径进行校正
	RedirectFixedPath bool

    // 这个值为true时,如果当前方法的路由没有被匹配到,会尝试匹配其他方法的路由,
    // 如果匹配到了则返回405,如果没有,就交给NotFound Handler处理
	HandleMethodNotAllowed bool

	// 这个值为true时,将开启OPTIONS自动匹配,注意: 手动匹配优先级更高
	HandleOPTIONS bool

    // 没有匹配到相应路由的时候会调用这个方法
    // 如果没有注册这个方法会返回 NotFound
	NotFound http.Handler

	// 没有匹配到相应路由并且HandleMethodNotAllowed为true时会调用这个方法
	MethodNotAllowed http.Handler

    // 用于从panic当中恢复
    // 需要返回500错误,并且渲染相应的错误页面
	PanicHandler func(http.ResponseWriter, *http.Request, interface{})
}

初始化 Router 之后看看路由是如何保存并且注册的

路由是如何保存的?

这里以官方 Readme 当中的例子说明:
如果注册了以下路由

r.GET("/", f1)
r.GET("/search/", f2)
r.GET("/support/", f3)
r.GET("/blog/", f4)
r.GET("/blog/:post/", f5)
r.GET("/about_us/", f6)
r.GET("/about_us/team/", f7)
r.GET("/contact/", f8)

那么这些路由会如下方所示,以一颗树的形式保存,并且这些路由的公共前缀会被抽离并且变为上一层节点
Priority 表示加上自身一共有多少个节点
Path 表示路径
Handle 表示路由注册的方法

Priority   Path             Handle
9          \                *<1>
3          ├s               nil
2          |├earch\         *<2>
1          |└upport\        *<3>
2          ├blog\           *<4>
1          |    └:post      nil
1          |         └\     *<5>
2          ├about-us\       *<6>
1          |        └team\  *<7>
1          └contact\        *<8>

r.Handle

r.Get, r.Post等方法实质都是通过调用 r.Handle 实现的

func (r *Router) Handle(method, path string, handle Handle) {
    // 路径注册必须从/开始,否则直接报错
	if path[0] != '/' {
		panic("path must begin with '/' in path '" + path + "'")
	}

    // 路由树map不存在需要新建
	if r.trees == nil {
		r.trees = make(map[string]*node)
	}

    // 获取当前方法所对应树的根节点,不存在则新建一个
	root := r.trees[method]
	if root == nil {
		root = new(node)
		r.trees[method] = root
	}

    // 向路由树当中添加一条一条路由
	root.addRoute(path, handle)
}

node

路由是注册到一颗路由树当中的,先看看节点的源码,再来分析,是如何添加路由的

type node struct {
    // 当前节点的路径
    path      string

    // 是否为参数节点,参数节点用:name表示
    wildChild bool

    // 当前节点类型, 一共有4种
    // static: 静态节点,默认类型
	// root: 根节点
	// param: 其他节点
	// catchAll: 带有*的节点,这里*的作用和正则当中的*一样
    nType     nodeType

    // 当前路径上最大参数的个数,不能超过255
    maxParams uint8

    // 代表分支的首字母
    // 上面的例子,当前节点为s
    // 那么indices = eu
    // ├s               nil
    // |├earch\         *<2>
    // |└upport\        *<3>
    indices   string

    // 孩子节点
    children  []*node

    // 注册的路由
    handle    Handle

    // 权重,表示当前节点加上所有子节点的数目
	priority  uint32
}

流程

httprouter使用了前缀树来存储路由和其对应的处理函数。

httprouter为每个http方法均创建了一颗前缀树。

type node struct {
	path      string  // 路径
	wildChild bool    // 路径是否有通配符(:或*)
	nType     nodeType // 节点类型,static(默认),root,param,catchAll中的一种,对应值为0,1,2,3
	maxParams uint8 // 当前节点及其子节点最大param数量
	priority  uint32 // 优先级
	indices   string // children的索引(孩子节点的第一个字母)
	children  []*node // 孩子节点
	handle    Handle // 路由处理函数
}

假如我们有下面这几个路径(GET方法):

/search/

/support

/blog/:post/

/blog/:post/:name

/files/*filepath

则创建的前缀树如下:

/              
├─ blog/            
│  └─ :post         
│     └─ /     
│        └─ :name   
├─ files           
│  └─ ""        
│     └─ /*filepath  
└─ s               
   ├─ earch        
   └─ upport       

插入代码为func (n *node) addRoute(path string, handle Handle)

插入即为前缀树的插入操作,从根节点开始,查找树是否有相同前缀,对无共同前缀的部分进行插入操作,在这个操作中,原来的节点非共同前缀部分会作为子节点。

要注意的是,插入分为有参数(有:或*符号)和无参数两类

1.path为最大能利用的公共前缀,当为叶子节点时,则为除去路径前缀的剩余部分(无参数)

2.如果有参数的路径,在上一步会进行额外操作,把参数部分裂开作为一个单独节点。如:/blog/:name/:id/,即使其为叶子节点,会把:name和:id作为单独节点

3.如果有通配符(*),则只能为最后一个节点。

查找

查找代码func (n *node) getValue(path string)

查找时同样分为有参数和无参数两类

1.对于无参数的路径,直接根据node.path匹配,根据node.indices进入下一节点顺着树查找即可。

2.对于有参数的路径,区分参数(:符号)和通配符(*),拿到路径对应的参数,也比较简单。

总结

1.httprouter使用前缀树来复用空间

2.代码简洁易懂

3.支持路径传参和通配

4.有优先级,保证匹配的确定性

纵览

New方法实际就是生成一个HttpRouter对象 接下来看看注册Get映射的实现

func (r *Router) GET(path string, handle Handle) {
    r.Handle(http.MethodGet, path, handle)
}
func (r *Router) Handle(method, path string, handle Handle) {
    if len(path) < 1 || path[0] != '/' {
        panic("path must begin with '/' in path '" + path + "'")
    }

    if r.trees == nil {
        r.trees = make(map[string]*node)
    }

    root := r.trees[method]
    if root == nil {
        root = new(node)
        r.trees[method] = root

        r.globalAllowed = r.allowed("*", "")
    }

    root.addRoute(path, handle)
}
func (n *node) addRoute(path string, handle Handle) {
    fullPath := path
    n.priority++
    numParams := countParams(path)

    // non-empty tree
    if len(n.path) > 0 || len(n.children) > 0 {
    walk:
        for {
            // Update maxParams of the current node
            if numParams > n.maxParams {
                n.maxParams = numParams
            }

            //查询最长公共前缀
            i := 0
            max := min(len(path), len(n.path))
            for i < max && path[i] == n.path[i] {
                i++
            }

            // 出现部分前缀匹配,最极端情况/根路径
            //创建一个以匹配部分为映射路径的空节点顶替当前匹配节点n,当前匹配节点n作为其子节点
            if i < len(n.path) {
                child := node{
                    path:      n.path[i:],
                    wildChild: n.wildChild,
                    nType:     static,
                    indices:   n.indices,
                    children:  n.children,
                    handle:    n.handle,
                    priority:  n.priority - 1,
                }

                // Update maxParams (max of all children)
                for i := range child.children {
                    if child.children[i].maxParams > child.maxParams {
                        child.maxParams = child.children[i].maxParams
                    }
                }

                n.children = []*node{&child}
                // []byte for proper unicode char conversion, see #65
                n.indices = string([]byte{n.path[i]})
                n.path = path[:i]
                n.handle = nil
                n.wildChild = false
            }
            //到这里可以确认,n绝对可以成为注册映射的祖先节点
            //找到最匹配的祖先节点作为自己的父节点
            if i < len(path) {
                path = path[i:]

                if n.wildChild {
                    n = n.children[0]
                    n.priority++

                    // Update maxParams of the child node
                    if numParams > n.maxParams {
                        n.maxParams = numParams
                    }
                    numParams--

                    // Check if the wildcard matches
                    if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
                        // Adding a child to a catchAll is not possible
                        n.nType != catchAll &&
                        // Check for longer wildcard, e.g. :name and :names
                        (len(n.path) >= len(path) || path[len(n.path)] == '/') {
                        continue walk
                    } else {
                        // Wildcard conflict
                        var pathSeg string
                        if n.nType == catchAll {
                            pathSeg = path
                        } else {
                            pathSeg = strings.SplitN(path, "/", 2)[0]
                        }
                        prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
                        panic("'" + pathSeg +
                            "' in new path '" + fullPath +
                            "' conflicts with existing wildcard '" + n.path +
                            "' in existing prefix '" + prefix +
                            "'")
                    }
                }

                c := path[0]

                // slash after param
                if n.nType == param && c == '/' && len(n.children) == 1 {
                    n = n.children[0]
                    n.priority++
                    continue walk
                }

                // Check if a child with the next path byte exists
                for i := 0; i < len(n.indices); i++ {
                    if c == n.indices[i] {
                        i = n.incrementChildPrio(i)
                        n = n.children[i]
                        continue walk
                    }
                }
                //插入子节点
                // Otherwise insert it
                if c != ':' && c != '*' {
                    // []byte for proper unicode char conversion, see #65
                    n.indices += string([]byte{c})
                    child := &node{
                        maxParams: numParams,
                    }
                    n.children = append(n.children, child)
                    n.incrementChildPrio(len(n.indices) - 1)
                    n = child
                }
                n.insertChild(numParams, path, fullPath, handle)
                return

            } else if i == len(path) { // Make node a (in-path) leaf
                if n.handle != nil {
                    panic("a handle is already registered for path '" + fullPath + "'")
                }
                n.handle = handle
            }
            return
        }
    } else { 
        //树是空的,直接作为root
        n.insertChild(numParams, path, fullPath, handle)
        n.nType = root
    }
}

然后查看其处理请求的方式,也就是对前缀树进行查询

func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    if r.PanicHandler != nil {
        defer r.recv(w, req)
    }

    path := req.URL.Path

    if root := r.trees[req.Method]; root != nil {
        //查询前缀树
        if handle, ps, tsr := root.getValue(path); handle != nil {
            //handle请求
            handle(w, req, ps)
            return
        } else if req.Method != http.MethodConnect && path != "/" {
            code := 301 // Permanent redirect, request with GET method
            if req.Method != http.MethodGet {
                // Temporary redirect, request with same method
                // As of Go 1.3, Go does not support status code 308.
                code = 307
            }

            if tsr && r.RedirectTrailingSlash {
                if len(path) > 1 && path[len(path)-1] == '/' {
                    req.URL.Path = path[:len(path)-1]
                } else {
                    req.URL.Path = path + "/"
                }
                http.Redirect(w, req, req.URL.String(), code)
                return
            }

            // Try to fix the request path
            if r.RedirectFixedPath {
                fixedPath, found := root.findCaseInsensitivePath(
                    CleanPath(path),
                    r.RedirectTrailingSlash,
                )
                if found {
                    req.URL.Path = string(fixedPath)
                    http.Redirect(w, req, req.URL.String(), code)
                    return
                }
            }
        }
    }

    if req.Method == http.MethodOptions && r.HandleOPTIONS {
        // Handle OPTIONS requests
        if allow := r.allowed(path, http.MethodOptions); allow != "" {
            w.Header().Set("Allow", allow)
            if r.GlobalOPTIONS != nil {
                r.GlobalOPTIONS.ServeHTTP(w, req)
            }
            return
        }
    } else if r.HandleMethodNotAllowed { // Handle 405
        if allow := r.allowed(path, req.Method); allow != "" {
            w.Header().Set("Allow", allow)
            if r.MethodNotAllowed != nil {
                r.MethodNotAllowed.ServeHTTP(w, req)
            } else {
                http.Error(w,
                    http.StatusText(http.StatusMethodNotAllowed),
                    http.StatusMethodNotAllowed,
                )
            }
            return
        }
    }

    // Handle 404
    if r.NotFound != nil {
        r.NotFound.ServeHTTP(w, req)
    } else {
        http.NotFound(w, req)
    }
}
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):]
                // If this node does not have a wildcard (param or catchAll)
                // child,  we can just look up the next child node and continue
                // to walk down the tree
                if !n.wildChild {
                    c := path[0]
                    for i := 0; i < len(n.indices); i++ {
                        if c == n.indices[i] {
                            n = n.children[i]
                            continue walk
                        }
                    }

                    // Nothing found.
                    // We can recommend to redirect to the same URL without a
                    // trailing slash if a leaf exists for that path.
                    tsr = (path == "/" && n.handle != nil)
                    return

                }

                // handle wildcard child
                n = n.children[0]
                switch n.nType {
                case param:
                    // find param end (either '/' or path end)
                    end := 0
                    for end < len(path) && path[end] != '/' {
                        end++
                    }

                    // save param value
                    if p == nil {
                        // lazy allocation
                        p = make(Params, 0, n.maxParams)
                    }
                    i := len(p)
                    p = p[:i+1] // expand slice within preallocated capacity
                    p[i].Key = n.path[1:]
                    p[i].Value = path[:end]

                    // we need to go deeper!
                    if end < len(path) {
                        if len(n.children) > 0 {
                            path = path[end:]
                            n = n.children[0]
                            continue walk
                        }

                        // ... but we can't
                        tsr = (len(path) == end+1)
                        return
                    }

                    if handle = n.handle; handle != nil {
                        return
                    } else if len(n.children) == 1 {
                        // No handle found. Check if a handle for this path + a
                        // trailing slash exists for TSR recommendation
                        n = n.children[0]
                        tsr = (n.path == "/" && n.handle != nil)
                    }

                    return

                case catchAll:
                    // save param value
                    if p == nil {
                        // lazy allocation
                        p = make(Params, 0, n.maxParams)
                    }
                    i := len(p)
                    p = p[:i+1] // expand slice within preallocated capacity
                    p[i].Key = n.path[2:]
                    p[i].Value = path

                    handle = n.handle
                    return

                default:
                    panic("invalid node type")
                }
            }
        } 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
            }

            if path == "/" && n.wildChild && n.nType != root {
                tsr = true
                return
            }

            // No handle found. Check if a handle for this path + a
            // trailing slash exists for trailing slash recommendation
            for i := 0; i < len(n.indices); i++ {
                if n.indices[i] == '/' {
                    n = n.children[i]
                    tsr = (len(n.path) == 1 && n.handle != nil) ||
                        (n.nType == catchAll && n.children[0].handle != nil)
                    return
                }
            }

            return
        }

        // Nothing found. We can recommend to redirect to the same URL with an
        // extra trailing slash if a leaf exists for that path
        tsr = (path == "/") ||
            (len(n.path) == len(path)+1 && n.path[len(path)] == '/' &&
                path == n.path[:len(n.path)-1] && n.handle != nil)
        return
    }
}

参考资料

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐