往期回顾:

  • gin源码解析 - gin 与 net/http 的关系

  • gin 源码解析 - 详解http请求在gin中的流转过程

上面两篇文章基本讲清楚了 Web Server 如何接收客户端请求,以及如何将请求流转到 gin 的逻辑。

gin 原理剖析说到这里,就完全进入 gin 的逻辑里面了。gin 已经拿到 http 请求了,第一件重要的事情肯定就是重写路由了,所以本节内容主要是分析 gin 的路由相关的内容。

其实 gin 的路由也不是完全自己写的,其实很重要的一部分代码是使用的开源的 julienschmidt/httprouter,当然 gin 也添加了部分自己独有的功能,如:routergroup。

什么是路由?

这个其实挺容易理解的,就是根据不同的 URL 找到对应的处理函数即可。

目前业界 Server 端 API 接口的设计方式一般是遵循 RESTful 风格的规范。当然我也见过某些大公司为了降低开发人员的心智负担和学习成本,接口完全不区分 GET/POST/DELETE 请求,完全靠接口的命名来表示。

举个简单的例子,如:"删除用户"

RESTful:    DELETE  /user/hhf
No RESTful: GET     /deleteUser?name=hhf

这种 No RESTful 的方式,有的时候确实减少一些沟通问题和学习成本,但是只能内部使用了。这种不区分 GET/POST 的 Web 框架一般设计的会比较灵活,但是开发人员水平参差不齐,会导致出现很多“接口毒瘤”,等你发现的时候已经无可奈何了,如下面这些接口:

GET /selectUserList?userIds=[1,2,3] -> 参数是否可以是数组?
GET /getStudentlist?skuIdCntMap={"200207366":1} -> 参数是否可以是字典?

这样的接口设计会导致开源的框架都是解析不了的,只能自己手动一层一层 decode 字符串,这里就不再详细铺开介绍了,等下一节说到 gin Bind 系列函数时再详细说一下。

继续回到上面 RESTful 风格的接口上面来,拿下面这些简单的请求来说:

GET    /user/{userID} HTTP/1.1
POST   /user/{userID} HTTP/1.1
PUT    /user/{userID} HTTP/1.1
DELETE /user/{userID} HTTP/1.1

这是比较规范的 RESTful API设计,分别代表:

  • 获取 userID 的用户信息

  • 更新 userID 的用户信息(当然还有其 json body,没有写出来)

  • 创建 userID 的用户(当然还有其 json body,没有写出来)

  • 删除 userID 的用户

可以看到同样的 URI,不同的请求 Method,最终其他代表的要处理的事情也完全不一样。

看到这里你可以思考一下,假如让你来设计这个路由,要满足上面的这些功能,你会如何设计呢?

gin 路由设计

如何设计不同的 Method ?

通过上面的介绍,已经知道 RESTful 是要区分方法的,不同的方法代表意义也完全不一样,gin 是如何实现这个的呢?

其实很简单,不同的方法就是一棵路由树,所以当 gin 注册路由的时候,会根据不同的 Method 分别注册不同的路由树。

GET    /user/{userID} HTTP/1.1
POST   /user/{userID} HTTP/1.1
PUT    /user/{userID} HTTP/1.1
DELETE /user/{userID} HTTP/1.1

如这四个请求,分别会注册四颗路由树出来。

func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {//....root := engine.trees.get(method)if root == nil {root = new(node)root.fullPath = "/"engine.trees = append(engine.trees, methodTree{method: method, root: root})}root.addRoute(path, handlers)// ...
}

其实代码也很容易看懂,

  • 拿到一个 method 方法时,去 trees slice 中遍历

  • 如果 trees slice 存在这个 method, 则这个URL对应的 handler 直接添加到找到的路由树上

  • 如果没有找到,则重新创建一颗新的方法树出来, 然后将 URL对应的 handler 添加到这个路由 树上

gin 路由的注册过程

func main() {r := gin.Default()r.GET("/ping", func(c *gin.Context) {c.JSON(200, gin.H{"message": "pong",})})r.Run() // listen and serve on 0.0.0.0:8080
}

这段简单的代码里,r.Get 就注册了一个路由 /ping 进入 GET tree 中。这是最普通的,也是最常用的注册方式。

不过上面这种写法,一般都是用来测试的,正常情况下我们会将 handler 拿到 Controller 层里面去,注册路由放在专门的 route 管理里面,这里就不再详细拓展,等后面具体说下 gin 的架构分层设计。

//controller/somePost.go
func SomePostFunc(ctx *gin.Context) {// do somethingcontext.String(http.StatusOK, "some post done")
}
```go
// route.go
router.POST("/somePost", controller.SomePostFunc)

使用 RouteGroup

v1 := router.Group("v1")
{v1.POST("login", func(context *gin.Context) {context.String(http.StatusOK, "v1 login")})
}

RouteGroup 是非常重要的功能,举个例子:一个完整的 server 服务,url 需要分为鉴权接口非鉴权接口,就可以使用 RouteGroup 来实现。其实最常用的,还是用来区分接口的版本升级。这些操作, 最终都会在反应到gin的路由树上

gin 路由的具体实现

func main() {r := gin.Default()r.GET("/ping", func(c *gin.Context) {c.JSON(200, gin.H{"message": "pong",})})r.Run() // listen and serve on 0.0.0.0:8080
}

还是从这个简单的例子入手。我们只需要弄清楚下面三个问题即可:

  • URL->ping 放在哪里了?

  • handler-> 放在哪里了?

  • URL 和 handler 是如何关联起来的?

1. GET/POST/DELETE/..的最终归宿

func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {return group.handle(http.MethodGet, relativePath, handlers)
}

在调用POST, GET, HEAD等路由HTTP相关函数时, 会调用handle函数。handle 是 gin 路由的统一入口。

// routergroup.go:L72-77
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {absolutePath := group.calculateAbsolutePath(relativePath)handlers = group.combineHandlers(handlers)group.engine.addRoute(httpMethod, absolutePath, handlers)return group.returnObj()
}

2. 生成路由树

下面考虑一个情况,假设有下面这样的路由,你会怎么设计这棵路由树?

GET /abc
GET /abd
GET /af

当然最简单最粗暴的就是每个字符串占用一个树的叶子节点,不过这种设计会带来的问题:占用内存会升高,我们看到 abc, abd, af 都是用共同的前缀的,如果能共用前缀的话,是可以省内存空间的。

gin 路由树是一棵前缀树. 我们前面说过 gin 的每种方法(POST, GET ...)都有自己的一颗树,当然这个是根据你注册路由来的,并不是一上来把每种方式都注册一遍。gin 每棵路由大概是下面的样子

图片

这个流程的代码太多,这里就不再贴出具体代码里,有兴趣的同学可以按照这个思路看下去即可。

3. handler 与 URL 关联

type node struct {path      stringindices   stringwildChild boolnType     nodeTypepriority  uint32children  []*node // child nodes, at most 1 :param style node at the end of the arrayhandlers  HandlersChainfullPath  string
}

node 是路由树的整体结构

  • children 就是一颗树的叶子结点。每个路由的去掉前缀后,都被分布在这些 children 数组里

  • path 就是当前叶子节点的最长的前缀

  • handlers 里面存放的就是当前叶子节点对应的路由的处理函数

当收到客户端请求时,如何找到对应的路由的handler?

《gin 源码阅读(2) - http请求是如何流入gin的?》第二篇说到 net/http 非常重要的函数 ServeHTTP,当 server 收到请求时,必然会走到这个函数里。由于 gin 实现这个 ServeHTTP,所以流量就转入 gin 的逻辑里面。

// gin.go:L439-443
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {c := engine.pool.Get().(*Context)c.writermem.reset(w)c.Request = reqc.reset()engine.handleHTTPRequest(c)engine.pool.Put(c)
}

所以,当 gin 收到客户端的请求时, 第一件事就是去路由树里面去匹配对应的 URL,找到相关的路由, 拿到相关的处理函数。其实这个过程就是 handleHTTPRequest 要干的事情。

func (engine *Engine) handleHTTPRequest(c *Context) {// ...t := engine.treesfor i, tl := 0, len(t); i < tl; i++ {if t[i].method != httpMethod {continue}root := t[i].root// Find route in treevalue := root.getValue(rPath, c.params, unescape)if value.params != nil {c.Params = *value.params}if value.handlers != nil {c.handlers = value.handlersc.fullPath = value.fullPathc.Next()c.writermem.WriteHeaderNow()return}if httpMethod != "CONNECT" && rPath != "/" {if value.tsr && engine.RedirectTrailingSlash {redirectTrailingSlash(c)return}if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {return}}break}// ...
}

从代码上看这个过程其实也很简单:

  • 遍历所有的路由树,找到对应的方法的那棵树

  • 匹配对应的路由

  • 找到对应的 handler

以上文章内容转载自公众号 HHFCodeRv ,作者haohongfan,京东架构师对Go语言研究颇深,喜欢他文章风格的推荐关注。

- END -

扫码关注公众号「网管叨bi叨」

给网管个星标,第一时间吸我的知识 

Go框架 gin 源码学习--路由的实现原理剖析相关推荐

  1. android tcp socket框架_最流行的 Web 框架 Gin 源码阅读

    最近公司大部分项目开始往golang换, api的框架选定使用gin, 于是将 gin的源码看了一遍, 会用几篇文章将gin的流程及流程做一个梳理, 下面进入正题. gin框架预览 上图大概是 gin ...

  2. spring源码学习之整合Mybatis原理分析

    本文主要解析spring是如何与mybatis进行整合,整合的过程中需要哪些组件的支持.以前面提到过的配置例子<spring源码学习之aop事物标签解析> 整合的过程中需要使用以下这个依赖 ...

  3. 【Android 源码学习】Zygote启动原理

    Android 源码学习 Zygote启动原理 望舒课堂 Zygote进程启动原理学习记录整理. Zygote简介 Zygote是进程在init进程启动时创建的,进程本身是app_process,来源 ...

  4. 【Android 源码学习】SystemServer启动原理

    Android 源码学习 SystemServer启动原理 望舒课堂 SystemServer进程启动原理学习记录整理. 参考文章: Android系统启动流程(三)解析SyetemServer进程启 ...

  5. flask源码学习-路由的注册与请求处理的过程

    Flask源码分析 本文环境python3.5.2,flask-1.0.2. Flask的路由注册 此时编写的脚本内容如下, from flask import Flaskapp = Flask(__ ...

  6. 蚂蚁金服分布式事务框架DTX源码学习

    文章目录 一.前言 二.DTX简介 三.角色 四.服务发起者与参与者DTX客户端启动流程 1.项目启动,创建dtx动态代理 2.初始化DtxClient客户端的init()方法 五.服务发起以及参与流 ...

  7. 集合框架-ArrayList源码学习

    MIT麻省理工学院讲义上的一段话: 如果没有完全理解 JAVA 库中的具有决定性的部分,你就不可能成为一个优秀的 JAVA 程序员.基本类型都包含在 java.lang 中.java.util包提供了 ...

  8. hystrix 源码 线程池隔离_Spring Cloud Hystrix 源码学习合集

    # Spring Cloud Hystrix 源码学习合集 **Hystrix: Latency and Fault Tolerance for Distributed Systems** ![](h ...

  9. Vue源码学习之Computed与Watcher原理

    前言  computed与watch是我们在vue中常用的操作,computed是一个惰性求值观察者,具有缓存性,只有当依赖发生变化,第一次访问computed属性,才会计算新的值.而watch则是当 ...

最新文章

  1. mac mysql utf 8编码_MacOS下MySQL设置UTF8编码问题
  2. 服务降级的概念及应用手段
  3. fpga中的slack_是否想减少部署过程的恐怖程度? 在Slack中构建ChatOps。
  4. Nginx通过地理位置限制访问
  5. Mac系统下SVN命令
  6. node-red mysql的增删改查_通过curl或者http请求对elasticsearch中的数据进行增删改查...
  7. SoapUI接口测试——添加新的API接口——new rest service from uri
  8. 光谱数据计算色彩指标的软件(功能强大,齐全)
  9. 超级牛散股神叶健颜专找重组题材股,精准买入,不服不行。
  10. Android显示——一帧的渲染过程(VSYNC)
  11. 京东方尚未成为苹果的最大供应商,但已经享受到果链的好处
  12. Java开发中常见的危险信号
  13. Bursuite简单抓包改包发包__超详细步骤
  14. 【开发工具】【windows】Visual Studio Code(VS Code)常用插件
  15. 2017年山东省机器人比赛 双足竟步 arduino源代码(删去了关键步态程序 gongneng1 和 gongneng2)
  16. linux ext4 img解包打包教程,安卓解包、修改、打包system.img/system.img.ext4教程
  17. 求1!+2!+...+10!的值
  18. 蓝桥杯培训试题新解——计算两个日期之间的天数间隔
  19. php文章cms插件,Phpcms v9百度神马后台勾选文章推送插件
  20. Matlab实验(一)

热门文章

  1. 《数学建模:基于R》——1.1 数据的描述性分析
  2. 白帽子发现美军网站SQL注入漏洞,可获取敏感数据
  3. 统计sql server数据库中所有表的记录数
  4. 《iOS网络编程与云端应用最佳实践》微博转发送书了!
  5. 【转】Android应用的自动升级、更新模块的实现 (2)
  6. 怎样才能有德国煤矿那样严密的安全网?
  7. ThinkPHP5框架接入阿里云短信最新版(原大鱼)的方法
  8. linux上docker安装centos7.2
  9. 利用汇编与机器码定位崩溃点
  10. Docker hello workd