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

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

gin 路由设计

RESTful 是区分方法的,不同的方法代表意义也完全不一样,gin 是如何实现这个的呢?

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

  1. GET /user/{userID} HTTP/1.1
  2. POST /user/{userID} HTTP/1.1
  3. PUT /user/{userID} HTTP/1.1
  4. DELETE /user/{userID} HTTP/1.1

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

  1. func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
  2. //....
  3. root := engine.trees.get(method)
  4. if root == nil {
  5. root = new(node)
  6. root.fullPath = "/"
  7. engine.trees = append(engine.trees, methodTree{method: method, root: root})
  8. }
  9. root.addRoute(path, handlers)
  10. // ...
  11. }

其实代码也很容易看懂,

  • 拿到一个 method 方法时,去 trees slice 中遍历
  • 如果 trees slice 存在这个 method, 则这个URL对应的 handler 直接添加到找到的路由树上
  • 如果没有找到,则重新创建一颗新的方法树出来, 然后将 URL对应的 handler 添加到这个路由 树上

gin 路由的注册过程

  1. func main() {
  2. r := gin.Default()
  3. r.GET("/ping", func(c *gin.Context) {
  4. c.JSON(200, gin.H{
  5. "message": "pong",
  6. })
  7. })
  8. r.Run() // listen and serve on 0.0.0.0:8080
  9. }

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

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

  1. //controller/somePost.go
  2. func SomePostFunc(ctx *gin.Context) {
  3. // do something
  4. context.String(http.StatusOK, "some post done")
  5. }
  1. // route.go
  2. router.POST("/somePost", controller.SomePostFunc)

使用 RouteGroup

  1. v1 := router.Group("v1")
  2. {
  3. v1.POST("login", func(context *gin.Context) {
  4. context.String(http.StatusOK, "v1 login")
  5. })
  6. }

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

gin 路由的具体实现

  1. func main() {
  2. r := gin.Default()
  3. r.GET("/ping", func(c *gin.Context) {
  4. c.JSON(200, gin.H{
  5. "message": "pong",
  6. })
  7. })
  8. r.Run() // listen and serve on 0.0.0.0:8080
  9. }

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

  • URL->ping 放在哪里了?
  • handler-> 放在哪里了?
  • URL 和 handler 是如何关联起来的?

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

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


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

  1. func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
  2. absolutePath := group.calculateAbsolutePath(relativePath)
  3. handlers = group.combineHandlers(handlers)
  4. group.engine.addRoute(httpMethod, absolutePath, handlers)
  5. return group.returnObj()
  6. }

2. 生成路由树

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

  1. GET /abc
  2. GET /abd
  3. GET /af

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

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

3. handler 与 URL 关联

  1. type node struct {
  2. path string
  3. indices string
  4. wildChild bool
  5. nType nodeType
  6. priority uint32
  7. children []*node // child nodes, at most 1 :param style node at the end of the array
  8. handlers HandlersChain
  9. fullPath string
  10. }

node 是路由树的整体结构

  • children 就是一颗树的叶子结点。每个路由的去掉前缀后,都被分布在这些 children 数组里
  • path 就是当前叶子节点的最长的前缀
  • handlers 里面存放的就是当前叶子节点对应的路由的处理函数

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

net/http 非常重要的函数 ServeHTTP,当 server 收到请求时,必然会走到这个函数里。由于 gin 实现这个 ServeHTTP,所以流量就转入 gin 的逻辑里面。

  1. func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
  2. c := engine.pool.Get().(*Context)
  3. c.writermem.reset(w)
  4. c.Request = req
  5. c.reset()
  6. engine.handleHTTPRequest(c)
  7. engine.pool.Put(c)
  8. }


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

  1. func (engine *Engine) handleHTTPRequest(c *Context) {
  2. httpMethod := c.Request.Method
  3. rPath := c.Request.URL.Path
  4. unescape := false
  5. if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
  6. rPath = c.Request.URL.RawPath
  7. unescape = engine.UnescapePathValues
  8. }
  9. if engine.RemoveExtraSlash {
  10. rPath = cleanPath(rPath)
  11. }
  12. // Find root of the tree for the given HTTP method
  13. t := engine.trees
  14. for i, tl := 0, len(t); i < tl; i++ {
  15. if t[i].method != httpMethod {
  16. continue
  17. }
  18. root := t[i].root
  19. // Find route in tree
  20. value := root.getValue(rPath, c.params, unescape)
  21. if value.params != nil {
  22. c.Params = *value.params
  23. }
  24. if value.handlers != nil {
  25. c.handlers = value.handlers
  26. c.fullPath = value.fullPath
  27. c.Next()
  28. c.writermem.WriteHeaderNow()
  29. return
  30. }
  31. if httpMethod != "CONNECT" && rPath != "/" {
  32. if value.tsr && engine.RedirectTrailingSlash {
  33. redirectTrailingSlash(c)
  34. return
  35. }
  36. if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
  37. return
  38. }
  39. }
  40. break
  41. }
  42. if engine.HandleMethodNotAllowed {
  43. for _, tree := range engine.trees {
  44. if tree.method == httpMethod {
  45. continue
  46. }
  47. if value := tree.root.getValue(rPath, nil, unescape); value.handlers != nil {
  48. c.handlers = engine.allNoMethod
  49. serveError(c, http.StatusMethodNotAllowed, default405Body)
  50. return
  51. }
  52. }
  53. }
  54. c.handlers = engine.allNoRoute
  55. serveError(c, http.StatusNotFound, default404Body)
  56. }


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

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