Skip to content

Latest commit

 

History

History
252 lines (214 loc) · 10.9 KB

gin.org

File metadata and controls

252 lines (214 loc) · 10.9 KB

gin源码分析

1 简介

gin1是一款优秀的http框架,看过的falcon2后端API也是基于gin开发的,对此框架的熟悉对快速实现上层基于HTTP的服务很有帮助,所以本文对其内部实现进行简要的说明。

2 Hello Word

package main

import "github.com/gin-gonic/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
}

这是官网首页上的一个Hello示例,服务启动时候的输出如下:

[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /ping                     --> main.main.func1 (3 handlers)
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080

上面的debug信息是由这两个函数输出:

  1. debugPrintWARNINGDefault
  2. debugPrintWARNINGNew

可以知道gin启动的时候会根据环境变量去设置在生产还是测试环境中。这在框架init时候就去做了,所以在main里没有看到。

// /home/ry/go/src/github.com/gin-gonic/gin/mode.go:40
func init() {
  mode := os.Getenv(ENV_GIN_MODE)
  SetMode(mode)
}

这里会去设置package level的变量: ginMode 后面所有的debug print都会去判断这个变量的值,包括上面提到的两个debug函数。

后面的输出是我们自己写的请求URL: /ping 后面却说有3个handlers,怎么回事?搞清楚怎么回事,先看这句话怎么输出的:

// call chain top down
func debugPrintRoute(httpMethod, absolutePath string, handlers HandlersChain)
func (engine *Engine) addRoute(method, path string, handlers HandlersChain)
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes

最上面负责输出,最下面是我们例子中调用的函数, gin.Default() 创建的是一个Engine, Engine里有个 RouterGroup,每层RouterGroup往下“串”的时候都会有一个Engine指针指向对应Engine,可以跟着函数调用路径看下,这里用了Golang struct的method promote语言属性。

我们解决了打印的疑问,但还没有解答这里的三个handlers问题,这里先放下,待阐述核心数据结构再说。

最后的输出同样指出PORT环境变量没有设置,代码在:

func resolveAddress(addr []string) string

至此一个简单HelloWorld的输出我们就全搞清楚了。

3 核心数据结构

在看核心数据结构之前,gin怎样attach到golang的http框架中的,

func (engine *Engine) Run(addr ...string) (err error) {
  defer func() { debugPrintError(err) }()

  address := resolveAddress(addr)
  debugPrint("Listening and serving HTTP on %s\n", address)
  err = http.ListenAndServe(address, engine)
  return
}

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request)

在Run函数中调用http.ListenAndServe,把gin的核心结构当做router(第二个参数)给到golang http框架,而engine肯定实现了 ServeHTTP这个接口:

// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
  c := engine.pool.Get().(*Context)
  c.writermem.reset(w)
  c.Request = req
  c.reset()

  engine.handleHTTPRequest(c)

  engine.pool.Put(c)
}

这里的代码看起来非常的简介优雅,意图十分明了,这种代码就是不需要注释的典范。

这里涉及的核心数据结构都用到了。Engine/Context,我们逐个说明解释。

3.1 Engine

Engine通过Default或者New函数来获得,这里不会贴代码,简述一下大概的结构:

  • 分配请求/应答核心数据结构Context的一个pool,在上面的 ServeHTTP 中可以看到,请求来的时候从pool里拿一个context实例,用完再放回去。
  • 路由tree描述,其实gin用的时httprouter3,采用经典的Trie结构来实现路由,不同的http method分别有独立的trie来跟进。gin的后端分配了一个大小为9的slice来存放不同的http method路由。查找路由时候遍历这个slice。
t := engine.trees
for i, tl := 0, len(t); i < tl; i++ {
  if t[i].method != httpMethod {
    continue
  }
  root := t[i].root
  // Find route in tree
  handlers, params, tsr := root.getValue(path, c.Params, unescape)

起初觉得这里为什么不用map而是要用slice,结果压测下来发现小size的slice查找竟然map的两倍多。所以这里看似O(n)的时间复杂度却要比map的O(1)要快,应该是map在算key时在小size时牺牲掉了。

3.2 bind

功能是怎么去读取body里面的内容,不同的类型有不同的操作方法。比如常见的json等。

3.3 render

功能时怎么去渲染结果,所以实现的都是怎么去write response,比如json等。

3.4 Context

context就是gin封装的对应每一个请求时用到的上下文。这里就会去call请求绑定、结果渲染等。

  • HTTP请求 http.Request
  • ResponseWriter
  • 指向Engine的指针
  • 相关参数

4 中间件的实现

解释之前的3个handler的疑问,答案在这里:

func Default() *Engine {
  debugPrintWARNINGDefault()
  engine := New()
  engine.Use(Logger(), Recovery())
  return engine
}

除了我们自己注册的一个回调,框架给我们加了两个, Logger Recovery 利用closure性质返回HandleFunc,一个负责打印请求,一个负责在异常时recover。 这里又涉及框架的中间件架构,我们从Use函数进入可以看到依赖 RouterGroup 这个实现。 RouterGroup对应的是应用层开发的那种REST感觉,比如 /user /admin 可以针对这样一个粒度来增加中间件,对应的内部的url写起来也轻松一点,一个很有说服力的例子如下:

u := r.Group("/api/v1/user")
u.GET("/auth_session", AuthSession)
u.POST("/login", Login)
u.GET("/logout", Logout)

//user modify
u.POST("/create", CreateUser)
authapi := r.Group("/api/v1/user")
authapi.Use(utils.AuthSessionMidd)
authapi.GET("/current", UserInfo)


alarmapi := r.Group("/api/v1/alarm")
alarmapi.Use(utils.AuthSessionMidd)
alarmapi.POST("/eventcases", AlarmLists)
alarmapi.GET("/eventcases", AlarmLists)
alarmapi.POST("/events", EventsGet)

这是falcon-plus中的源码,可以看到,各模块的URL都是相对这个group写的,这样看起来更清爽,创建的Engine中有一个默认的RouterGroup,也可以称为根RouterGroup。每创建一个RouterGroup时都会计算出当前的basePath,所以可以看出这里是可以嵌套的。那么这些callchain怎么串起来的?答案是:

func (c *Context) Next() {
  c.index++
  for s := int8(len(c.handlers)); c.index < s; c.index++ {
    c.handlers[c.index](c)
  }
}

中间件放在一个list中,每个中间件会call这个函数进行级联调用。那么第一个谁来call呢?当然是router咯,具体参见

func (engine *Engine) handleHTTPRequest(c *Context) {
...
      handlers, params, tsr := root.getValue(path, c.Params, unescape)
      if handlers != nil {
        c.handlers = handlers
        c.Params = params
        c.Next()
        c.writermem.WriteHeaderNow()
        return
      }

...
}

这里找到注册的handlers之后由框架开启第一个Next调用,然后开启整个调用chain。注意这里的顺序,自己注册回调是在list的最后一个,中间件在前,所以logger中间件的实现的closure是放在第一个(我们的helloworld示例),进去获取开始时间就调用Next,只到chain都处理完回到这里就可以计算出总共消耗多少时间,这也是中间件的一个使用技巧,即Next的调用不是在结尾,而是在某处。

添加路由/获取路由这个模块是基于httproute高性能router,有兴趣可以继续深入下去。

4.1 请求URL绑定

router.GET("/user/:name", func(c *gin.Context) {
    name := c.Param("name")
    c.String(http.StatusOK, "Hello %s", name)
})

这样的URL在添加注册时由 func (n *node) addRoute(path string, handlers HandlersChain) 调用 func (n *node) insertChild(numParams uint8, path string, fullPath string, handlers HandlersChain) 来完成标记,

// github.com/gin-gonic/gin/tree.go:289
// 这里完成注册
      child := &node{
        nType:     param,
        maxParams: numParams,
      }

// github.com/gin-gonic/gin/tree.go:401
// 这里完成获取操作

需要注意 c.Param 这个函数的时间复杂度是O(N)的,即在所有参数里找到第一个,所以这里URL传参不宜过多,代码里规定上限255个。

4.2 HTTP get参数

// /welcome?firstname=Jane&lastname=Doe
router.GET("/welcome", func(c *gin.Context) {
    firstname := c.DefaultQuery("firstname", "Guest")
    lastname := c.Query("lastname") // shortcut for c.Request.URL.Query().Get("lastname")

    c.String(http.StatusOK, "Hello %s %s", firstname, lastname)
})

和注释里的一样,这个只是顺着 context 找到http request然后调用标准库中的URL方法获取URL参数,没什么好说的。

4.3 HTTP post参数获取

name := c.PostForm("name")

这里调用标准库的 ParseForm 实现,将结果绑定到请求的Form上。

gin里还有一个 c.ShouldBind(obj interface{}) 用于绑定参数到结构体,这个实现会根据请求方法和MIME来判断然后通过各自的方法来实现对应的绑定,这里的实现除了支持JSON对象绑定竟然还是Protobuf,所以这里的反序列化都可以由框架来做,不得不觉得框架设计者的用心。

5 一些sugar

这个文件里一堆 github.com/gin-gonic/gin/context.go:678 这里就不列举了。有帮助返回的,有方便建立临时JSON对象的(gin.H)

6 总结

总体看Gin的代码质量很高,很具有学习价值,后面有空继续扩充吧,官方首页的README写的太详细基本后端API开发够了。

7 Footnotes

1 https://github.com/gin-gonic/gin

2 https://github.com/open-falcon/falcon-plus

3 https://github.com/julienschmidt/httprouter