vlambda博客
学习文章列表

gin源码分析(二)——gin.GET()等路由绑定方法解读

下面的代码摘自gin官方文档快速入门。

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

上一篇源码分析讲到了,gin.Default()最终返回的是一个*Engine类型,然而在我们点进这个声明好的实例调用的GET方法的时候,却神奇地发现,这个实例调用的竟然是RouterGroup的方法!

点进去,发现这个方法实现自IRoutes的接口,而分别又有如下的几个结构体实现了IRoutes接口。

gin源码分析(二)——gin.GET()等路由绑定方法解读

点进去Engine,我试图找到它内部的GET方法,结果又让我大为震惊!

gin源码分析(二)——gin.GET()等路由绑定方法解读

竟然没有GET这个方法!

我大为震惊,在查阅了许多资料之后,发现了这样的一个名词:embeded type。该名词中文直译为内嵌类型。这种内部内嵌的操作,一共分为四类:

  • 接口中内嵌接口。相当于合并了内部接口,实现外部接口也需要将内部接口的所有方法给实现了。
  • 接口中内嵌结构体。报错,Interface 不能嵌入非interface的类型。
  • 结构体中内嵌接口。初始化时,内嵌接口要用一个实现此接口的结构体赋值,此外, 外层结构体中,只能调用内层接口定义的函数,而不能调用实现内层接口的结构体的其他方法或该结构体的字段。外层结构体,可以作为receiver,重新定义同名函数,这样可以覆盖内层内嵌结构中定义的函数,同时,可以用外层结构体引用内嵌接口的实例,并调用内嵌接口的函数。
  • 结构体内嵌结构体,初始化,内嵌结构体要进行赋值,外 层结构体自动获得内嵌结构体的所有字段和方法,外层结构体可以定义同名方法来覆盖内嵌结构体的方法,也可以定义同名变量,覆盖内层结构体的变量。同样可以内层结构体引用,内层结构体中已经定义的方法和变量。内部内嵌的形式如下所示:
type A struct{
 B
}

type B struct{
 Name string
}
func (b B) String() string,error{
 return b.Name
}

这样,A结构体不仅继承了B中的Name字段,同时也拥有了String()的方法。这是因为go语言不支持Java等中的继承而给出的一种新的实现,这种实现叫做内嵌。

回到正文,我们终于可以明白为什么Engine可以调用GET方法了,虽然Engine没有实现这个方法,但是Engine内嵌了一个RouterGroup,而RouterGroup实现了GET等方法,因此Engine拥有了RouterGroup的所有方法,同样的,正因如此,原本没有实现IRoutes接口的Engine,也被认为实现了这个接口。此时,再看官方示例,r := gin.Default()而不是e := gin.Default()就很有深意了。

再来看GET方法的内容。

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

可以看到第一个入参类型是string,从参数名可以看出,这是一个路径,这个路径与这个RouterGroup关联。之后是一个不定参,这个参数是HandlerFunc类型的,是一个处理这个路由链路发送过来的数据的函数。在这个方法里,调用了RouterGroup的handle方法。并传入了一个常量http.MethodGet,之后再将该函数的入参传入。

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()
}

在这个方法中,会首先调用RouterGroup.calculateAbsolutePath()方法,将relativePath包装为携带着RouterGroup的basePath的一个路径,例如,我们给的这个路由的路径为/ping,但是这个路由所在的组的基路径(basePath)为/api/v1,则通过这个方法,会将relativePath包装成/api/v1/ping,这样将这条路径与这个RouterGroup给分离开,抽象为独立的一个路径,之后,通过调用path.Join()方法,将这个路径给放到缓存中去。整个方法调用路径如下:

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()
}

func (group *RouterGroup) calculateAbsolutePath(relativePath string) string {
 return joinPaths(group.basePath, relativePath)
}

func joinPaths(absolutePath, relativePath string) string {
 if relativePath == "" {
  return absolutePath
 }

 finalPath := path.Join(absolutePath, relativePath)
 if lastChar(relativePath) == '/' && lastChar(finalPath) != '/' {
  return finalPath + "/"
 }
 return finalPath
}

func Join(elem ...string) string {
 size := 0
 for _, e := range elem {
  size += len(e)
 }
 if size == 0 {
  return ""
 }
 buf := make([]byte0, size+len(elem)-1)
 for _, e := range elem {
  if len(buf) > 0 || e != "" {
   if len(buf) > 0 {
    buf = append(buf, '/')
   }
   buf = append(buf, e...)
  }
 }
 return Clean(string(buf))
}

可以看到,最后返回了一个string类型的新路径,这个路径自此就加入了整个路径字典树。加入RouterGroup是为了便于开发人员对路由链路进行管理,而不是依赖于这个RouterGroup。

回到handle函数中,接下来调用的是combileHandlers函数,这个函数的函数体如下:

func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
 finalSize := len(group.Handlers) + len(handlers)
 if finalSize >= int(abortIndex) {
  panic("too many handlers")
 }
 mergedHandlers := make(HandlersChain, finalSize)
 copy(mergedHandlers, group.Handlers)
 copy(mergedHandlers[len(group.Handlers):], handlers)
 return mergedHandlers
}

在这个函数中,会首先获取group.Handlers现有的所有Handler,然后再向这个切片中加入新加的handlers函数。最后返回的是一个更新后的所有Handler的集合。

之后,就会调用engine的addRoute()方法,这个方法的方法体如下:

func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
 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")

 debugPrintRoute(method, path, handlers)

 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)

 // Update maxParams
 if paramsCount := countParams(path); paramsCount > engine.maxParams {
  engine.maxParams = paramsCount
 }
}

这个函数入参有方法,路径,和对应的函数。在这个方法中,会首先将这些路由信息给打印出来。之后,通过engine.trees.get()和这个入参中的方法名,获取当前方法的方法字典树的节点。在这个节点中将路径和函数绑定起来。最后,返回一个IRoutes的实现结构体。

至此,整个gin.GET()方法执行结束。

gin源码分析(二)——gin.GET()等路由绑定方法解读



 
   
   
 

我们是程序员khaos,一群酷爱编程,乐于分享的小伙子,下期见~


「分享」「点赞」「在看」是最大支持