gin是一款小巧高性能的web服务器框架,本文就gin的一些工作流程做一些介绍!
我们在使用Gin框架的时候,大致流程为:
这里我们先预设几个问题:
- Gin是怎么接管Golang的http服务的?
- 初始化Gin的时候初始化了什么内容?
- 我们创建的中间件最Gin内部是怎么存储怎么使用的?
- 创建的接口是怎么存储跟怎么使用的?
- 如何调用?
问题1:Gin是怎么接管Golang的http服务的?
记得我们是如何使用Gin的吗?如下:
server := &http.Server{
Addr: addr,
Handler: router.Get(),
ReadTimeout: config.App.ReadTimeout,
WriteTimeout: config.App.WriteTimeout,
MaxHeaderBytes: 1 << 20,
}
_ = server.ListenAndServe()
其中,我们会将自己定义的api接口对应golang内部http.Server结构中的Handler,当我们追踪源码到Server结构中的时候可以发现Handler其实是个接口:
Server结构体:
type Server struct{
...
Handler Handler
...
}
Handler接口:
type Handler interface{
ServeHTTP(ResponseWriter,*Request)
}
在Go中我们清楚,只要实现了某个接口,那么接收该接口的地方同样可以接收实现了这个接口的实例。知道这一点我们可以去Gin的源码中查找对应的方法就可以知道了。
经过查找我们找到,确实在gin.go文件中,Gin的实现Engine存在这个方法的实现:
// 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)
}
所以现在我们知道了,gin是通过该方法让golang原生的http将接口请求放置到该方法中的。
问题2:初始化Gin的时候初始化了什么内容?
我们可以简单的贴一下gin的实例,New() *Engin函数中的内容,再根据里面的参数针对几个特别的做个解释:
func New() *Engine {
debugPrintWARNINGNew()
engine := &Engine{
RouterGroup: RouterGroup{
Handlers: nil,
basePath: "/",
root: true,
},
FuncMap: template.FuncMap{},
RedirectTrailingSlash: true,
RedirectFixedPath: false,
HandleMethodNotAllowed: false,
ForwardedByClientIP: true,
AppEngine: defaultAppEngine,
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
engine.pool.New = func() interface{} {
return engine.allocateContext()
}
return engine
}
参数注解:
RouterGroup:关于中间件,接口的创建存储都是从这里开始的。
RemoveExtraSlash:设置为true可以解决api路径出现错误数量的/ . 的情况。
trees:用来存储api接口
pool:对象复用池,防止被gc。
需要知道的是,在编译执行后(初始化的时候),程序将会把我们创建的中间件,接口全部存储到内存中(切片)。
问题3:我们创建的中间件最Gin内部是怎么存储怎么使用的?
在gin中,我们使用use使用一个中间件,所以可以根据该方法来查找,中间件被创建之后,是存储在哪里的,通过查找,发现是在RouterGroup中的Handlers里面,并且是一个自定义的类型:HandlersChain,该自定义的原始类型为:HandlersChain []HandlerFunc,HandlerFunc任然是一个自定义类型,它的原始类型为:type HandlerFunc func(*Context),这个func里面的Context是gin自定义的Context。
//routergroup.go
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
group.Handlers = append(group.Handlers, middleware...)
return group.returnObj()
}
可以看到所有的中间件都是存储在group.Handlers中的。
关于中间件是怎么使用的,这里暂时不讲,等到跟api接口一起来探讨。
问题4:创建的接口是怎么存储跟怎么使用的?
接口的创建有两种:
- 使用Group创建api分组
- 直接创建api接口
其实,无论是使用group创建还是直接创建,内部都是通过RouterGroup结构来创建。我们照样通过源码来证明:
我们首先在代码中写一个简单的案例来查看:
//通过group创建
r.Group("/aa")
//直接创建
r.GET("/a", func(c *gin.Context) {
c.String(http.StatusOK, "successful ")
})
然后我们点击Group跟GET方法进去:
//routergroup.go
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
return &RouterGroup{
Handlers: group.combineHandlers(handlers),
basePath: group.calculateAbsolutePath(relativePath),
engine: group.engine,
}
}
//routergroup.go
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodGet, relativePath, handlers)
}
可以很清楚的看到,GET是属于RouterGroup的方法,所以实际上,当我们使用GET直接创建后,实际上使用的是初始化的时候的那个RouterGroup对象,然后当我们主动使用Group的时候,是表示额外再创建一个RouterGroup对象。
我们的API处理函数的类型都为:HandlerFunc,继续拿Group(relativePath string, handlers …HandlerFunc)方法来看,该方法的第二个可变参数表示的是属于该组的中间件。
提问:中间件的作用是为了在请求到达接口之前做些额外的处理,当我们有直接通过实例中的默认RouterGroup创建的中间件跟自己创建的Group里面创建的中间件,这两个地方的中间件是如何在一个api请求前同时生效的呢?
解答:
RouterGroup中存有一个HandlersChain类型的Handlers这是一个切片。在我们使用Group创建一个api组的时候,会通过group.combineHandlers(handlers)将外部初始化时的中间件复制到当前自定义的RouterGroup里面的Handlers中:
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里面的时候,你要知道它并不是中间件的终点,它的终点是在api接口上面。
api在创建的时候会调用handle方法将前面的中间件集合放在当前api处理函数之前,这样做的目的是为了保证中间件的执行时间在处理函数之前!
//routergroup.go
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 (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)
}
我们可以看到,实际上最终api接口的归宿在于gin实例engine中的tress对象。该对象是一个methodTree的切片类型:
type methodTrees []methodTree
type methodTree struct {
method string
root *node
}
type node struct {
path string
indices string
children []*node
handlers HandlersChain
priority uint32
nType nodeType
maxParams uint8
wildChild bool
fullPath string
}
func (n *node) addRoute(path string, handlers HandlersChain) {
.....
// Empty tree
if len(n.path) == 0 && len(n.children) == 0 {
n.insertChild(numParams, path, fullPath, handlers)
n.nType = root
return
}
.....
}
所有的接口最后都按照http请求方法不同,全部放在tress中。
并且每个接口都含有全部的公共中间件。
问题5:如何调用?
内部调用任然是在实现ServeHTTP方法的地方
//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)
}
func (engine *Engine) handleHTTPRequest(c *Context) {
....
if engine.RemoveExtraSlash {
rPath = cleanPath(rPath)
}
// Find root of the tree for the given HTTP method
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
value := root.getValue(rPath, c.Params, unescape)
if value.handlers != nil {
c.handlers = value.handlers
c.Params = value.params
c.fullPath = value.fullPath
c.Next()
c.writermem.WriteHeaderNow()
return
}
....
}
....
}
当value := root.getValue(rPath, c.Params, unescape)获取到存储到的接口之后,会通过c.Next()方法去调用,Next是不是很熟悉?没错,这个方法经常被用在中间件里面。
//context.go
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}
可以看到方法中通过for循环来依次遍历当前接口的handlers,handlers集合前面部分存放的是中间件(如果有的话),后面才是api处理函数。
c.index默认值是-1,所以第一次进来需要进行自增操作。因为Next方法是整个gin框架调用中间件跟api处理函数的地方,并且单个api接口跟中间件全部在同一个切片集合,所以当在某个中间件里面调用next之后,那么该中间件里面Next方法之后的操作将在该接口所拥有的所有中间件方法跟api处理函数之后执行!