gin1是一款优秀的http框架,看过的falcon2后端API也是基于gin开发的,对此框架的熟悉对快速实现上层基于HTTP的服务很有帮助,所以本文对其内部实现进行简要的说明。
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信息是由这两个函数输出:
- debugPrintWARNINGDefault
- 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的输出我们就全搞清楚了。
在看核心数据结构之前,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,我们逐个说明解释。
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时牺牲掉了。
功能是怎么去读取body里面的内容,不同的类型有不同的操作方法。比如常见的json等。
功能时怎么去渲染结果,所以实现的都是怎么去write response,比如json等。
context就是gin封装的对应每一个请求时用到的上下文。这里就会去call请求绑定、结果渲染等。
- HTTP请求 http.Request
- ResponseWriter
- 指向Engine的指针
- 相关参数
解释之前的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,有兴趣可以继续深入下去。
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个。
// /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参数,没什么好说的。
name := c.PostForm("name")
这里调用标准库的 ParseForm
实现,将结果绑定到请求的Form上。
gin里还有一个 c.ShouldBind(obj interface{})
用于绑定参数到结构体,这个实现会根据请求方法和MIME来判断然后通过各自的方法来实现对应的绑定,这里的实现除了支持JSON对象绑定竟然还是Protobuf,所以这里的反序列化都可以由框架来做,不得不觉得框架设计者的用心。
这个文件里一堆 github.com/gin-gonic/gin/context.go:678
这里就不列举了。有帮助返回的,有方便建立临时JSON对象的(gin.H)
总体看Gin的代码质量很高,很具有学习价值,后面有空继续扩充吧,官方首页的README写的太详细基本后端API开发够了。
1 https://github.com/gin-gonic/gin