gin源码
gin是go开发的一个开源高性能http框架,其主要是把go官方的net/http
进行了扩展,前缀树实现了动态路由、支持了中间件、对请求信息进行封装方便用户层使用等。本文基于 gin v1.7.2
版本
创建流程
一个Engine实例可以使用New
或者 Default
进行创建,唯一区别就是Default
默认增加了两个中间件:日志Logger(), panic捕获 Recovery()
初始化会初始化以下内容:
//gin.go
engine := &Engine{
//默认的分组
RouterGroup: RouterGroup{
Handlers: nil,
basePath: "/",
root: true,
},
FuncMap: template.FuncMap{},
RedirectTrailingSlash: true,
RedirectFixedPath: false,
HandleMethodNotAllowed: false,
ForwardedByClientIP: true,
RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
TrustedProxies: []string{"0.0.0.0/0"},
TrustedPlatform: defaultPlatform,
UseRawPath: false,
RemoveExtraSlash: false,
UnescapePathValues: true,
MaxMultipartMemory: defaultMultipartMemory,
trees: make(methodTrees, 0, 9),
delims: render.Delims{Left: "{{", Right: "}}"},
secureJSONPrefix: "while(1);",
}
engine.RouterGroup.engine = engine
//Context上下文Pool
engine.pool.New = func() interface{} {
return engine.allocateContext()
}
初始化好后,就可以注册业务的相关api,比如GET、POST等。默认情况下所有的api都是在根分组下,举个例子:
r := gin.Default()
//此处的api所在的分组是默认分组,所以请求api的时候直接 /ping即可
r.GET("/ping", func(c *gin.Context) {
c.String(http.StatusOK, "pong")
})
下面说一下注册api的流程:
//routergroup.go
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
//2. 将相对路径转为绝对路径
//主要就是 分组的路径+请求的路径,比如:分组为 v1/,请求路径是 hello ,这个请求的全路径就是 v1/hello
absolutePath := group.calculateAbsolutePath(relativePath)
//3. 因为gin支持中间件,这里是把组携带的handler和传递过来的请求函数进行组合
handlers = group.combineHandlers(handlers)
//4. 将中间件和请求函数的组合放入路由中
//这样的话,一次api请求,会执行一系列的函数集,达到中间件的效果
//因为中间件是属于组的,所以一个组下的所有api都支持
group.engine.addRoute(httpMethod, absolutePath, handlers)
return group.returnObj()
}
//1. 对外提供的http方法
//其他POST DELETE PUT 等注册流程都一样
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodGet, relativePath, handlers)
}
初始化好一个Engine,并且注册了api后,就可以运行服务,对外使用了。
// main.go
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.String(http.StatusOK, "pong")
})
r.Run(":9000")
}
// gin.go
func (engine *Engine) Run(addr ...string) (err error) {
defer func() { debugPrintError(err) }()
err = engine.parseTrustedProxies()
if err != nil {
return err
}
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
//gin实现了ServeHTTP(w http.ResponseWriter, req *http.Request)
//所以注册到http服务
err = http.ListenAndServe(address, engine)
return
}
因为官方 net/http
提供了接口:ServeHTTP(ResponseWriter, *Request)
,gin实现了接口:
//gin.go
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)
}
所以在底层收到http消息后,会回调gin实现的ServeHTTP,这样http消息就可以走gin提供的路由、中间件等逻辑了
请求流程
- 发起http请求
- 底层回调gin注册函数
ServeHTTP
- 从sync.pool中获取一个可用
Context
- 因为是结构体并且sync.pool机制不会主动重置
Context
,所以手动重置Context
- 从前缀树中寻找对应路由
- 执行请求对应的函数
- 将结果写入响应
Response
Context
放回sync.pool中
//gin.go
//底层回调gin注册函数`ServeHTTP`
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
//从sync.pool中获取一个可用`Context`
c := engine.pool.Get().(*Context)
c.writermem.reset(w)
c.Request = req
//因为是结构体并且sync.pool机制不会主动重置`Context`,所以手动重置`Context`
c.reset()
//执行请求
engine.handleHTTPRequest(c)
engine.pool.Put(c)
}
从pool中获取,如果没有会进行创建,创建函数是在初始化的时候注册的。
//gin.go
func New() *Engine {
//...
engine.pool.New = func() interface{} {
return engine.allocateContext()
}
}
func (engine *Engine) allocateContext() *Context {
v := make(Params, 0, engine.maxParams)
return &Context{engine: engine, params: &v}
}
分组与中间件
分组的作用
分组的好处是将其下的所有api进行统一管理,如果没有分组,增加一个通用功能,就需要对每一个api分别添加。比如:对/admin
开头的路由进行鉴权,gin中只需要这样做:
gAdmin:=r.Group("/admin").Use(func(c *gin.Context) {
//鉴权
})
gAdmin.GET("/delUser", func(c *gin.Context) {})
gAdmin.GET("/addUser", func(c *gin.Context) {})
当用户请求 /admin/delUser
和/admin/addUser
时,会先执行鉴权函数。Use
也就是增加中间件的方法。
分组的路由
另外一个路由的添加是由分组地址+api的地址组合而成,初始化Engine
的时候会默认有个根组它的basePath
为 /
:
//gin.go
func New() *Engine {
engine := &Engine{
RouterGroup: RouterGroup{
Handlers: nil,
basePath: "/",
root: true,
},
//...
}
//...
return engine
}
如果不创建其他组,使用默认组的话:
func main() {
r := gin.Default()
//请求路由为 group.basePath+ `/ping` = http://127.0.0.1/ping
r.GET("/ping", func(c *gin.Context) {
c.String(http.StatusOK, "pong")
})
}
分组有父子关系,下面这个a
分组派生于根分组,所以a
分组下的api路由是 /a/xxx
,b分组下的api路由是/a/b/xxx
,c
分组由根分组派生,所以c
分组下的api路由是 /c
func main() {
r := gin.Default()
aGroup := r.Group("/a")
bGroup := aGroup.Group("/b")
cGroup := r.Group("/c")
}
路由如此,中间件也会如此,组b
下的api包含所有父组的中间件:
//routergroup.go
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
return &RouterGroup{
//新的组包含父辈的所有中间件
Handlers: group.combineHandlers(handlers),
basePath: group.calculateAbsolutePath(relativePath),
engine: group.engine,
}
}
中间件的执行
现在知道了分组和路由的关系,看看中间件是如何执行的。gin在注册一个api的时候,会把组中的中间件函数和api函数放到数组里,增加到路由里:
//routergroup.go
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
absolutePath := group.calculateAbsolutePath(relativePath)
//将中间件和api函数组合,中间件在数组前面 api函数在其后
handlers = group.combineHandlers(handlers)
group.engine.addRoute(httpMethod, absolutePath, handlers)
return group.returnObj()
}
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
finalSize := len(group.Handlers) + len(handlers)
assert1(finalSize < int(abortIndex), "too many handlers")
mergedHandlers := make(HandlersChain, finalSize)
copy(mergedHandlers, group.Handlers)
copy(mergedHandlers[len(group.Handlers):], handlers)
return mergedHandlers
}
来了一个请求后,找到对应路由下的函数放到Context
上下文中,调用Next
执行,并且要注意的是所有的中间件所有的函数都用的同一个Context
func (engine *Engine) handleHTTPRequest(c *Context) {
//..
value := root.getValue(rPath, c.params, unescape)
if value.params != nil {
c.Params = *value.params
}
if value.handlers != nil {
c.handlers = value.handlers
c.fullPath = value.fullPath
c.Next()
c.writermem.WriteHeaderNow()
return
}
//..
}
这里说一下Next执行细节,用个例子来说明:对分组a
下所有api请求计时
func Logger(c *gin.Context) {
//开始计时
t := time.Now()
//调用下一个函数
c.Next()
//计算用时
latency := time.Since(t)
log.Print(latency)
}
func Hello(c *gin.Context){
fmt.Println("hello")
}
func main() {
r := gin.Default()
aGroup := r.Group("/a").Use(Logger)
aGroup.GET("b", Hello)
r.Run(":9000")
}
- 初始化Context
func (c *Context) reset() {
c.Writer = &c.writermem
c.Params = c.Params[:0]
c.handlers = nil
//index字段是-1
c.index = -1
c.fullPath = ""
c.Keys = nil
c.Errors = c.Errors[:0]
c.Accepted = nil
c.queryCache = nil
c.formCache = nil
*c.params = (*c.params)[:0]
}
- 调用函数 Next(index=0)
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}
- 执行了Logger函数: 开始计时
- Logger内部执行c.Next,再次调用Next函数
- 因为index是c中的变量,所以会变成index=1
- 所以此时执行了Hello
- Hello执行后因为index已经变成2了,所以Next完结
- Logger因c.Next()完成,继续执行后续操作:计算时间 打印时间
- 完成了整个api调用
路由
上面说的各种流程都没讲一个请求过来,是如何找到具体的执行函数的,这里就是路由的作用了。用map
存路由表,索引效率高效,但只支持静态路由。类似/hello/:name
可以匹配到 /hello/wang
/hello/zhang
的动态路由不支持。gin里使用了前缀树来实现。前缀树就不在这里介绍了
创建路由
下面注册了5个api,从源码看看是如何执行的
func main() {
r := gin.New()
r.GET("/index", func(c *gin.Context) {
c.JSON(200, "index")
})
r.GET("/inter", func(c *gin.Context) {
c.JSON(200, "inter")
})
r.GET("/user/get", func(c *gin.Context) {
c.JSON(200, "/user/get")
})
r.GET("/user/del", func(c *gin.Context) {
c.JSON(200, "/user/del")
})
r.GET("/user/:name", func(c *gin.Context) {
c.JSON(200, "/user/:name")
})
r.Run(":9000")
}
func (n *node) addRoute(path string, handlers HandlersChain) {
fullPath := path
n.priority++
// 第一个api注册因为根节点path和children是空的所以直接成为根节点的子节点
if len(n.path) == 0 && len(n.children) == 0 {
n.insertChild(path, fullPath, handlers)
n.nType = root
return
}
parentFullPathIndex := 0
walk:
for {
// 获得第一次字符不同的位置 比如
// "/index" 和 "/inter" 第一次字符不同的位置 也就是 i=3
i := longestCommonPrefix(path, n.path)
if i < len(n.path) {
child := node{
path: n.path[i:],
wildChild: n.wildChild,
indices: n.indices,
children: n.children,
handlers: n.handlers,
priority: n.priority - 1,
fullPath: n.fullPath,
}
/*当前node增加一个子节点child 以 inter为例子,api inter插入之前已经有了index,并且发现他们有相同字符in所以将index节点改为 in节点,dex变成子in的子节点,后面的代码会再将 ter放到in子节点中。
child := node{
path: n.path[i:], // dex
wildChild: n.wildChild, // false
indices: n.indices, // ""
children: n.children, //null
handlers: n.handlers, // index func
priority: n.priority - 1,
fullPath: n.fullPath, // /index
}
n.indices = "d"
n.path = "/in"
n.fullPath = ""
*/
n.children = []*node{&child}
n.indices = bytesconv.BytesToString([]byte{n.path[i]})
n.path = path[:i]
n.handlers = nil
n.wildChild = false
n.fullPath = fullPath[:parentFullPathIndex+i]
}
if i < len(path) {
path = path[i:]
c := path[0]
if n.nType == param && c == '/' && len(n.children) == 1 {
parentFullPathIndex += len(n.path)
n = n.children[0]
n.priority++
continue walk
}
// 以user/del为例;
// 此时根节点的indices= "iu" 匹配到了相同字符 `u` 于是进行跳转,并将n指向 path="user/" 的节点
for i, max := 0, len(n.indices); i < max; i++ {
if c == n.indices[i] {
parentFullPathIndex += len(n.path)
i = n.incrementChildPrio(i)
n = n.children[i]
continue walk
}
}
if c != ':' && c != '*' && n.nType != catchAll {
//以插入inter api为例:
// 此时n.indices = "dt" dex和ter的首字母
n.indices += bytesconv.BytesToString([]byte{c})
child := &node{
fullPath: fullPath,
}
n.addChild(child)
n.incrementChildPrio(len(n.indices) - 1)
//这里这么写是方便后面流程通用 看 FF:
n = child
} else if n.wildChild {
n = n.children[len(n.children)-1]
n.priority++
if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
n.nType != catchAll &&
(len(n.path) >= len(path) || path[len(n.path)] == '/') {
continue walk
}
pathSeg := path
if n.nType != catchAll {
pathSeg = strings.SplitN(pathSeg, "/", 2)[0]
}
prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
panic("'" + pathSeg +
"' in new path '" + fullPath +
"' conflicts with existing wildcard '" + n.path +
"' in existing prefix '" + prefix +
"'")
}
n.insertChild(path, fullPath, handlers)
return
}
//FF:
if n.handlers != nil {
panic("handlers are already registered for path '" + fullPath + "'")
}
n.handlers = handlers
n.fullPath = fullPath
return
}
}
最后这个前缀树的结构应该是这样的:
api请求
engine.trees
这是一个数组,每个请求类型(POST GET PUT…)独立一个树
type methodTree struct {
method string
root *node
}
type methodTrees []methodTree
当接收到底层传来的http请求后,先找到指定请求类型的树结构,然后再查询路由,查询方式比较简单,主要就是遍历树。为了提高查询效率,indices的作用就是在查询子节点之前,先找indices里有没有请求的path首字符,没有的话直接查询失败。
//gin.go
func (engine *Engine) handleHTTPRequest(c *Context) {
//...
httpMethod := c.Request.Method
t := engine.trees
for i, tl := 0, len(t); i < tl; i++ {
if t[i].method != httpMethod {
continue
}
root := t[i].root
//查询路由
value := root.getValue(rPath, c.params, unescape)
if value.params != nil {
c.Params = *value.params
}
if value.handlers != nil {
c.handlers = value.handlers
c.fullPath = value.fullPath
c.Next()
c.writermem.WriteHeaderNow()
return
}
//...
}
}