go web框架 gin-gonic源码解读02————router
本来想先写context,但是发现context能简单讲讲的东西不多,就准备直接和router合在一起讲好了
router是web服务的路由,是指讲来自客户端的http请求与服务器端的处理逻辑或者资源相映射的机制。(这里简单说说,详细的定义网上都可以查到)
那一个优秀的web router应该提供以下功能:
- URL解析:路由的过程始于URL解析。URL是一个标识资源位置的字符串,通常由协议、主机名、端口号和路径组成。服务器需要解析这个URL,以便找到对应的处理程序或资源。
- 路由规则:在Web应用程序中,通常会定义一组路由规则,这些规则将特定的URL模式与相应的处理程序或资源进行绑定。路由规则可以基于URL路径、HTTP方法(GET、POST等)、查询参数和其他条件来匹配请求。
- 动态路由:除了静态路由(固定的URL与处理程序映射)外,现代Web应用程序还支持动态路由。动态路由允许使用参数将URL与处理程序关联起来。这样,同一种类型的请求可以通过不同的参数调用不同的处理程序,实现更灵活和通用的路由。
- …
为了展示gin框架的router的优越性,我们先看看go原生的net/http处理(不是说net/http不好,只是说说他的一些小缺点,不要网暴我) 这里摘抄部分net/http默认的路由器的代码
// GO\src\net\http\server.go
// ServeMux就是我们net/http默认的router
type ServeMux struct {
mu sync.RWMutex // 大家都知道为了并发安全,搞得读锁
m map[string]muxEntry // 这个map就是我们路由的核心
es []muxEntry // slice of entries sorted from longest to shortest.
// 为了前缀匹配搞得切片,后面func match() 方法你一看就知道他是干什么用的了
hosts bool // whether any patterns contain hostnames
}
// 上一篇文章说过的每个Server都要实现的ServeHTTP(w ResponseWriter, r *Request)接口会调用这个方法来获取要执行的h Handler(可以理解为这个传参path的这个url对应的逻辑)
func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
mux.mu.RLock()
defer mux.mu.RUnlock()
// Host-specific pattern takes precedence over generic ones
// 本代码段行6的波尔值,这个配置一般是有要区别不同主机会有不同逻辑的时候才会用
if mux.hosts {
// 这个match就是我们net/http的匹配算法
h, pattern = mux.match(host + path)
}
if h == nil {
// 这个match就是我们net/http的匹配算法
h, pattern = mux.match(path)
}
if h == nil {
// 默认的404处理返回函数
h, pattern = NotFoundHandler(), ""
}
return
}
// 匹配算法 (非常的简单,特别是和后面的gin比起来)
func (mux *ServeMux) match(path string) (h Handler, pattern string) {
// Check for exact match first.
// 很简单直接去map里find,其实效率也还可以,但是这里你就能看出来他只能支持静态路由,map这里也不支持模糊搜索
v, ok := mux.m[path]
if ok {
//
return v.h, v.pattern
}
// Check for longest valid match. mux.es contains all patterns
// that end in / sorted from longest to shortest.
// 这里英文注释也蛮清楚的,就是map里找不到,这里找一下以入参path为前缀的url。
// 并且这个mux.es还是有序的,为了提升一点效率,从这里看他似乎也不是完全静态的。
for _, e := range mux.es {
if strings.HasPrefix(path, e.pattern) {
return e.h, e.pattern
}
}
return nil, ""
}
这里篇幅有限只讲讲net/http的match,inster就不说了。
从上面的macth代码也可以看出net/http的路由存在以下缺点
-
缺乏灵活的路由定义:net/http 包的路由定义相对简单,只能通过 http.HandleFunc 或 http.Handle 来定义路由处理函数。这导致难以支持复杂的路由模式,如正则表达式匹配、参数提取等
-
不支持动态路由:这个包并没有原生支持动态路由,即不能直接将路由和参数绑定起来,需要手动解析 URL。
-
不支持中间件:中间件是一种常用的扩展路由处理的方法,可以在请求到达路由处理函数之前或之后执行一些操作。然而,net/http 包没有内置的中间件支持,需要手动编写和管理中间件。
-
不支持子路由:在一些应用场景中,需要将不同类型的请求映射到不同的处理函数,这些处理函数可能共享某些共同的前缀。net/http 包并没有内置的子路由支持,需要自己实现。
-
不支持路由组:在一些情况下,希望将一组相关的路由规则进行分组管理,以便更好地组织代码。net/http 包没有原生支持路由组的功能。
接下来正片开始,讲router主要讲两个函数:match(路由匹配),insert(路由注册)
gin的路由数据结构
和大多说的web框架一样,gin选择了使用前缀树算法,来进行路由匹配,因为确实蛮合适的。前缀树这边不细讲了,蛮简单的,大家可Google看看。这里直接撸gin的源码。
// ../gin/tree.go
// static:表示静态节点。静态节点是指路由路径中没有参数或通配符的节点,其值是固定的字符串。例如,路径 "/home" 中的 "home" 就是一个静态节点。
// root:表示根节点。根节点是整个路由树的顶级节点,它没有路径,其作用是起始点,用于构建路由树的根结构。
// param:表示参数节点。参数节点是指路由路径中的一部分可以是变量的节点,例如 "/user/:id" 中的 ":id" 就是一个参数节点,可以匹配任意值。
// catchAll:表示通配符节点。通配符节点是指路由路径中的一部分可以匹配任意内容的节点,例如 "/static/*filepath" 中的 "*filepath" 就是一个通配符节点,可以匹配以 "/static/" 开头的所有路径。
type nodeType uint8
const (
static nodeType = iota
root
param
catchAll
)
// 这个结构存放的是每个方法的根节点,如GET,POST,PUT,他们的根节点也是这种方式在Engine中存储的
// type Engine struct {
// ...
// // 简单来书就是每种请求方法是一颗独立的前缀树
// trees methodTrees
// ...
// }
type methodTrees []methodTree
// 一个简单的get方法
func (trees methodTrees) get(method string) *node {
for _, tree := range trees {
if tree.method == method {
return tree.root
}
}
return nil
}
type methodTree struct {
method string
root *node
}
// 树的各个节点
type node struct {
// 到该节点的路由路径片段,例如"/home",那他就是hemo
path string
// 索引,下文细讲
indices string
// 子节点中,是否是有通配符节点,有的话插入新的节点时,要注意维护通配符节点是最后一个节点
wildChild bool
// 上文提到的该节点的类型
nType nodeType
// 优先级,下文细讲
priority uint32
// 子节点,gin以每个出现差异的开始下标分割单前阶段和孩子节点
children []*node
// 所有的中间件和对应的url处理的逻辑函数,后续讲中间件的时候细讲为什么中间件和服务逻辑函数写在一个数组里
handlers HandlersChain
// 全路径
fullPath string
}
路由器的节点插入(路由绑定)
使用过gin框架的同学都知道,我们在使用go-gin时,只需要 gin.GET(“/hello/world”, helloworld) 这么一句简单的代码就可以实现url"/hello/world" 和 逻辑函数helloworld()的绑定,接下来让我们看看func GET()里都发生了什么。
func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes {
if matched := regEnLetter.MatchString(httpMethod); !matched {
panic("http method " + httpMethod + " is not valid")
}
return group.handle(httpMethod, relativePath, handlers)
}
// POST is a shortcut for router.Handle("POST", path, handlers).
func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodPost, relativePath, handlers)
}
// GET is a shortcut for router.Handle("GET", path, handlers).
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodGet, relativePath, handlers)
}
当然不止get和post,gin还定义各种其他的http的请求方法,但是都大同小异,这边以get和post举例。
从这里可以看出来不管是什么http的方法最终调用的都是Handle 方法,并且讲请求方法作以string的方式传入。比如注释上说的
// POST is a shortcut for router.Handle(“POST”, path, handlers)
func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers …HandlerFunc) IRoutes {}
而Handle()方法之中又调用了一个handle方法
// gin.go
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
// (group *RouterGroup) RouterGroup 大家常用也很熟悉,就是我们的url分组,我们开发中会为每个url分组设置一个统一的前缀url,
// group.calculateAbsolutePath(relativePath)这一步是为了帮助我们拼接处正确的url全文
absolutePath := group.calculateAbsolutePath(relativePath)
// handlers 就是我们这篇文章上面所讲的node 结构体中的handlers 。
// 这一步是为了把你注册路由绑定的服务逻辑方法绑定到调用链上,这个下一章讲中间件的时候会细讲
handlers = group.combineHandlers(handlers)
// 终于到了最终的一步!AddRouter,这个方法里就是我们路由器的节点插入(路由绑定)核心方法
group.engine.addRoute(httpMethod, absolutePath, handlers)
return group.returnObj()
}
// 核心方法 addRoute (这里为了篇幅我会做一些摘抄)
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
// 做一些断言,检测一下输入的url是否合法,比较越早阶段的暴露问题是程序的铁律
assert1(path[0] == '/', "path must begin with '/'")
assert1(method != "", "HTTP method can not be empty")
assert1(len(handlers) > 0, "there must be at least one handler")
// debug模式下一些日志打印(不太重要)
debugPrintRoute(method, path, handlers)
// engine.tree中存储了一个http请求方法的字符串为根节点的切片,所以这里我们拿http请求方法method先get
// 这个get方法在上文有,大家可以自己拉上去看看
root := engine.trees.get(method)
// 一个好的设计肯定不可能一开始把所有的可能性都构造出来,这边也是,管你是get还是post,都是用到了再创建,然后插入。
if root == nil {
root = new(node)
root.fullPath = "/"
// 这种http请求方法是第一次出现,这边创建一个,并且插入
engine.trees = append(engine.trees, methodTree{method: method, root: root})
}
// 核心的核心来了,大家请看下面的代码块tree.go
root.addRoute(path, handlers)
// Update maxParams
if paramsCount := countParams(path); paramsCount > engine.maxParams {
engine.maxParams = paramsCount
}
if sectionsCount := countSections(path); sectionsCount > engine.maxSections {
engine.maxSections = sectionsCount
}
}
tree.go
// tree.go
// 核心的核心非常的长
func (n *node) addRoute(path string/*绑定的url路径*/, handlers HandlersChain/*绑定的逻辑函数*/) {
fullPath := path
// 优先级++,主要用于每个节点子节点排序,提高查找的效率
n.priority++
// Empty tree
if len(n.path) == 0 && len(n.children) == 0 {
n.insertChild(path, fullPath, handlers)
n.nType = root
return
}
parentFullPathIndex := 0
walk:
for {
// Find the longest common prefix.
// This also implies that the common prefix contains no ':' or '*'
// since the existing key can't contain those chars.
// 大家注意哈,我们这里进循环了,请开始转动你的小脑瓜
// 就是找当前节点的路径和你patch最长的相同的前缀,并且放回下标
i := longestCommonPrefix(path, n.path)
// Split edge
// 显然现在的path和该节点的path有不相同的地方,说明我们的前缀树要开始分叉了
// 接下来我们要做的就是把我们传入的path从不相同的地方开始拆分成两个节点。
if i < len(n.path) {
// 创建一个孩子节点, 初始化的时候我们先把大部分的值设置的和我们的当前节点一致
child := node{
// 这里就是我说的拆分的地方,你看从两者字符串不同的地方的下标剪断了
path: n.path[i:],
wildChild: n.wildChild,
nType: static,
indices: n.indices,
children: n.children,
handlers: n.handlers,
// 这个开始拆分就表示他至少会有两个子节点,优先级就降低了
priority: n.priority - 1,
fullPath: n.fullPath,
}
// 由于一个分支下的所有孩子节点都会有相同的前缀,也就是他们父节点所记录的值,这里字符出现了不同
// 其实就是我们当前节点成了父节点,我们要把相同的部分留给当前节点,然后不同部分分成两个新的子节点
// 所以这里先把当前节点拆了,拆一个子节点出来。
n.children = []*node{&child}
// []byte for proper unicode char conversion, see #65
// 这段代码看完也基本解开了indices这个索引变量的神秘面纱了,他其实就是该节点的所有字节的首字符
// 拼接在一起的一个字符串,方便后面的查找,这里只有一个i是因为他把当前节点拆成了0-i和i-len两个节点
// 在我们path插入之前他肯定只有一个孩子节点。
n.indices = bytesconv.BytesToString([]byte{n.path[i]})
n.path = path[:i]
// 当前节点现在被拆分了,他现在只是前缀树上的一个分叉节点,它本身并不代表某个url,所以给他的这两个参数置为nil和false
n.handlers = nil
n.wildChild = false
n.fullPath = fullPath[:parentFullPathIndex+i]
}
// Make new node a child of this node
// 当前节点虽然已经被拆成了两个节点:父节点(当前node与我们字符匹配的部分)--> 子节点(当前node与我们字符不匹配的部分)
// 当时我们自己的path还没有变成节点插入呢。这里会有两种情况,一种是字符串匹配下标i小于path的长度或者大于等于path的长度
// 这里我们分开处理,先说 i < len(path) 这种情况下我们tree会是下图这种情况
// 父节点(当前node与我们字符匹配的部分)
// |--> 子节点1(当前node与我们字符不匹配的部分)
// |--> 字节点2(我们的path)
// 这里其实就两种情况。1.i<len(path);2.i=len(path);1情况下面还有几种情况要处理
// 而情况2相当于当前节点就是我们的path了,需要给他绑定逻辑函数handlers
if i < len(path) {
// path的前半段path[0-i]已经被当前节点node表示,所以这里裁掉
path = path[i:]
c := path[0]
// '/' after param
// 这里处理一种特殊情况,当前节点是param参数节点,且他只有一个子节点,并且我们用来查找的字符串是'/'
// 那就这个节点这里两者是可以匹配上的,那我们直接把当前节点变成子节点继续匹配。
if n.nType == param && c == '/' && len(n.children) == 1 {
parentFullPathIndex += len(n.path)
// continue了,请大家带着当前所有的记忆返回 代码行walk:
n = n.children[0]
n.priority++
continue walk
}
// Check if a child with the next path byte exists
// 遍历当前节点的所有索引,其实就是看看孩子节点里有没有哪个首字符和我们path首字符一样的
for i, max := 0, len(n.indices); i < max; i++ {
if c == n.indices[i] {
parentFullPathIndex += len(n.path)
// 有的话这个孩子就是我们的当前节点了,所以我们要维护一下这个children节点,并且再次拿到他的下标
// 维护的方法incrementChildPrio()这个的代码我贴在下面了
i = n.incrementChildPrio(i)
n = n.children[i]
// continue了,请大家带着当前所有的记忆返回 代码行walk:
continue walk
}
}
// Otherwise insert it
// 走到这一步,说明当前节点的所有的子节点都没有匹配上,我们先看看要插入的path是否是匹配路径(c != ':' && c != '*')
// 再看看我们的当前节点是否是匹配节点(n.nType != catchAll)
if c != ':' && c != '*' && n.nType != catchAll {
// 如果都不是,那说明是个正常节点和正常path,那我们把这个path当这个正常子节点,先给他创造结构体,后续统一插入
// []byte for proper unicode char conversion, see #65
// 更新当前节点的索引
n.indices += bytesconv.BytesToString([]byte{c})
// 创建结构体,等待后续插入
child := &node{
fullPath: fullPath,
}
// 给当前节点插入子节点
n.addChild(child)
// 维护索引n.indices有序
n.incrementChildPrio(len(n.indices) - 1)
n = child
} else if n.wildChild {
// 到这一步说明当前节点的子节点中有通配符节点,那我们直接取子节点的最后一个节点
// (插入子节点的时候我们会特意维护,通配符节点的最后一个,这样子取用起来也很方便)
// inserting a wildcard node, need to check if it conflicts with the existing wildcard
n = n.children[len(n.children)-1]
n.priority++
// Check if the wildcard matches
// 检查n是否和path匹配(这里n = n.children[len(n.children)-1]了)
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 整个再来一次
continue walk
}
// Wildcard conflict
// 这都没匹配上,说明出问题了,这里拼接一下错误,panic了
// 一般是同一级分支下出现了两个同级的通配符 ,示例可以看下文的func TestTreePanic1(t *testing.T)
pathSeg := path
if n.nType != catchAll {
pathSeg = strings.SplitN(pathSeg, "/", 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是通配符(c == ':' || c == '*')
// 或n是全匹配节点(n.nType != catchAll)
// 并且都当前节点没有通配符子节点,直接插入
n.insertChild(path, fullPath, handlers)
return
}
// Otherwise add handle to current node
if n.handlers != nil {
panic("handlers are already registered for path '" + fullPath + "'")
}
// 迭代到这里了就是当前节点n就是我们path了,需要最后做一些赋值
n.handlers = handlers
n.fullPath = fullPath
return
}
}
func longestCommonPrefix(a, b string) int {
i := 0
max := min(len(a), len(b))
for i < max && a[i] == b[i] {
i++
}
return i
}
// Increments priority of the given child and reorders if necessary
// 这里主要做了一个排序操作,维护了一下node的子节点切片,使他们以priority的 大小为规则排序
// 排序之后我们要拿的那个儿子节点的下标有可能会改变,所以还要返回一个维护过得newpos来保证返回的是正确的坐标
func (n *node) incrementChildPrio(pos int) int {
cs := n.children
// 优先级现先++
cs[pos].priority++
prio := cs[pos].priority
// Adjust position (move to front)
newPos := pos
// 经典冒泡,根据优先级priority进行排序
for ; newPos > 0 && cs[newPos-1].priority < prio; newPos-- {
// Swap node positions
cs[newPos-1], cs[newPos] = cs[newPos], cs[newPos-1]
}
// Build new index char string
if newPos != pos {
n.indices = n.indices[:newPos] + // Unchanged prefix, might be empty
n.indices[pos:pos+1] + // The index char we move
n.indices[newPos:pos] + n.indices[pos+1:] // Rest without char at 'pos'
}
return newPos
}
func findWildcard(path string) (wildcard string, i int, valid bool) {
// Find start
for start, c := range []byte(path) {
// A wildcard starts with ':' (param) or '*' (catch-all)
if c != ':' && c != '*' {
continue
}
// Find end and check for invalid characters
valid = true
for end, c := range []byte(path[start+1:]) {
switch c {
case '/':
return path[start : start+1+end], start, valid
case ':', '*':
valid = false
}
}
return path[start:], start, valid
}
return "", -1, false
}
func (n *node) insertChild(path string, fullPath string, handlers HandlersChain) {
for {
// Find prefix until first wildcard
// 这个就是查找path是否有通配符的'*',':','/'
// 没有查到直接break
// wildcard拿的是中间那段比如:/:w/hello 那wildcard就是:w
wildcard, i, valid := findWildcard(path)
if i < 0 { // No wildcard found
break
}
// The wildcard name must only contain one ':' or '*' character
if !valid {
panic("only one wildcard per path segment is allowed, has: '" +
wildcard + "' in path '" + fullPath + "'")
}
// check if the wildcard has a name
if len(wildcard) < 2 {
panic("wildcards must be named with a non-empty name in path '" + fullPath + "'")
}
// 如果wildcard首字符是':'拼装child,把wildcard之前的不是通配符区块的,拼接到n的path中
if wildcard[0] == ':' { // param
if i > 0 {
// Insert prefix before the current wildcard
n.path = path[:i]
path = path[i:]
}
child := &node{
nType: param,
path: wildcard,
fullPath: fullPath,
}
n.addChild(child)
n.wildChild = true
n = child
n.priority++
// if the path doesn't end with the wildcard, then there
// will be another subpath starting with '/'
if len(wildcard) < len(path) {
path = path[len(wildcard):]
child := &node{
priority: 1,
fullPath: fullPath,
}
n.addChild(child)
n = child
continue
}
// Otherwise we're done. Insert the handle in the new leaf
n.handlers = handlers
return
}
// catchAll
if i+len(wildcard) != len(path) {
panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'")
}
if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
pathSeg := strings.SplitN(n.children[0].path, "/", 2)[0]
panic("catch-all wildcard '" + path +
"' in new path '" + fullPath +
"' conflicts with existing path segment '" + pathSeg +
"' in existing prefix '" + n.path + pathSeg +
"'")
}
// currently fixed width 1 for '/'
i--
if path[i] != '/' {
panic("no / before catch-all in path '" + fullPath + "'")
}
n.path = path[:i]
// First node: catchAll node with empty path
child := &node{
wildChild: true,
nType: catchAll,
fullPath: fullPath,
}
n.addChild(child)
n.indices = string('/')
n = child
n.priority++
// second node: node holding the variable
child = &node{
path: path[i:],
nType: catchAll,
handlers: handlers,
priority: 1,
fullPath: fullPath,
}
n.children = []*node{child}
return
}
// If no wildcard was found, simply insert the path and handle
n.path = path
n.handlers = handlers
n.fullPath = fullPath
}
这里放一些panic的或者正常的测试代码实例方便大家理解
func TestTreePanic1(t *testing.T) {
tree := &node{}
routes := [...]string{
"/hi/",
"/hi/:go",
"/hi/:go1",
}
for _, route := range routes {
tree.addRoute(route, fakeHandler(route))
}
}