最近项目中的几个模块用到了Gin框架,记录一下看源码的心得。
Gin框架是一个go开源的轻量级http服务器框架
官方文档:https://gin-gonic.com/docs/
源码地址:https://github.com/gin-gonic/gin
中文教程:https://learnku.com/docs/gin-gonic/2019
主要有以下功能特性:
a、基于redixtree的路由策略,没有使用反射,占用内存小。采用sync.Pool来缓存频繁更新的Context结构体,大大减小了gc开销
b、提供了简单的中间件注册来实现扩展性,大大提升了请求处理的灵活性
c、通过路由group,提供方便和全面的路由管理
d、提供了包括GET/POST/BIND的全面、便捷的获取参数的方式
e、简单且使用的json、xml和html渲染等
f、能够抓取http请求的panic并恢复。
先看业务代码:
// GetRouter 获取web路由
func GetRouter() *gin.Engine {
engine := gin.Default()
engine.GET("/metrics", gin.WrapH(promhttp.Handler()))
//日志中间件
engine.Use(context.GinLogger())
//监控中间件,用来做接口调用次数,延时,错误等上报
engine.Use(action.PromReportMiddleware())
engine.GET("ping", context.GinHandleContext(func(c *context.Context) {
c.Log.Traceln("recv ping")
c.GeneralResponse(gin.H{
"message": "pong",
})
}))
//Controller struct中添加相应的接口, 接口满足GinHandlerFunc,即可用register_route.RegisterRoute进行接口注册
//并且只需要注册一次Controller struct即可。
//每次添加接口不需要其他操作,只要保证Controller struct中只有接口函数,没有其他多余函数即可
r := engine.Group("/Actual")
return engine
}
gin.Default()初始化gin的Engine结构体,查看源码,先看看这个Engine结构体长啥样子。
// Engine is the framework's instance, it contains the muxer, middleware and configuration settings.
// Create an instance of Engine, by using New() or Default()
type Engine struct {
RouterGroup
pool sync.Pool
trees methodTrees
....
}
几个比较重要的成员RouterGroup,匿名成员在go里属于集成,engine结构继承了RouterGroup这个结构体所有的成员和方法。可以看到这个结构体是用来存储路由和其对应方法链的,可以理解为一个路由组处理方法的公共处理部分,后面介绍中间件方法被如何调用的时候会进一步介绍。
type RouterGroup struct {
Handlers HandlersChain
basePath string
engine *Engine
root bool
}
pool是Context包的pool,gin采用这个的方式来存储每一个请求的Context,类似创建了一个内存池,需要处理请求的时候会从这个pool里面取一个出来(如果没有,则创建一个,使用完后放进去。sync.pool的使用可以参见这片博客:https://www.cnblogs.com/qcrao-2018/p/12736031.html)。这么做的好处是,不需要频繁的gc,极大的减小了因为gc导致的开销。trees保存的是对应路由的处理方法,后面介绍路由注册的时候会讲到。
Default函数调用了New函数,初始化了一个Engine结构体。
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery())
return engine
}
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
}
可以看到,最后几行,pool的New函数会分配一个Context,并初始化这个Context的engine指针指向刚刚申请的engine结构体。
func (engine *Engine) allocateContext() *Context {
return &Context{engine: engine}
}
来看中间件注册的use函数。
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
engine.RouterGroup.Use(middleware...)
engine.rebuild404Handlers()
engine.rebuild405Handlers()
return engine
}
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
group.Handlers = append(group.Handlers, middleware...)
return group.returnObj()
}
直接把中间件的函数以slice append的方式,放在了engine结构体成员RouterGroup下的Handlers这个slice中。
以post为例,介绍路由注册。
func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodPost, relativePath, handlers)
}
再看RouterGroup的handle方法,可以看到还是小写,不能被外部调用的。
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()
}
注意看这个handle函数的第二行,group.combineHandlers(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
}
engine继承了RouterGroup的方法,在combineHandlers这个函数里面,先把路由组里的handlers放在mergedHandlers这个slice里面,然后再把路由注册的方法,依次放在后面,这样就实现了中间件注册的函数总是在路由注册的函数执行之前执行。
再看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)
}
还记得前面engine结构体里面的trees成员么。这里先去路由树里找这个method(post get 之类的http方法)的路由,如果是第一次加入则创建一个根节点,否则直接加路由。redixtree的路由插入过程这里不做叙述,后续会更新文章讲解。
engine初始化完成,中间件与路由注册完成以后,engine就要启动,开始服务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
}
可以看到,最终还是依赖了golang自带的http库,并调用了ListenAndServe这个函数开始执行监听与服务响应。注意到,这里除了传入一个address参数以外,还把我们之前创建的engine这个结构体也传递了进去。继续挖ListenAndServe这个函数:
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
新建了一个http库自带的Server结构体,并把engine传入。
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
到这里发现,Handler是一个interface,再去看看engine有没有实现这个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实现了这个interface,那接下来的问题就是:http库响应http请求,如何回到我们传入的engine的ServeHTTP接口?先记住,我们这里new的这个Server的handler是engine结构体,他实现了ServerHTTP这样一个interface。
继续看server.ListenAndServe()
func (srv *Server) ListenAndServe() error {
if srv.shuttingDown() {
return ErrServerClosed
}
addr := srv.Addr
if addr == "" {
addr = ":http"
}
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return srv.Serve(ln)
}
最后走到了Serve函数:
func (srv *Server) Serve(l net.Listener) error {
if fn := testHookServerServe; fn != nil {
fn(srv, l) // call hook with unwrapped listener
}
origListener := l
l = &onceCloseListener{Listener: l}
defer l.Close()
if err := srv.setupHTTP2_Serve(); err != nil {
return err
}
if !srv.trackListener(&l, true) {
return ErrServerClosed
}
defer srv.trackListener(&l, false)
baseCtx := context.Background()
if srv.BaseContext != nil {
baseCtx = srv.BaseContext(origListener)
if baseCtx == nil {
panic("BaseContext returned a nil context")
}
}
var tempDelay time.Duration // how long to sleep on accept failure
ctx := context.WithValue(baseCtx, ServerContextKey, srv)
for {
rw, err := l.Accept()
if err != nil {
select {
case <-srv.getDoneChan():
return ErrServerClosed
default:
}
if ne, ok := err.(net.Error); ok && ne.Temporary() {
if tempDelay == 0 {
tempDelay = 5 * time.Millisecond
} else {
tempDelay *= 2
}
if max := 1 * time.Second; tempDelay > max {
tempDelay = max
}
srv.logf("http: Accept error: %v; retrying in %v", err, tempDelay)
time.Sleep(tempDelay)
continue
}
return err
}
connCtx := ctx
if cc := srv.ConnContext; cc != nil {
connCtx = cc(connCtx, rw)
if connCtx == nil {
panic("ConnContext returned nil")
}
}
tempDelay = 0
c := srv.newConn(rw)
c.setState(c.rwc, StateNew) // before Serve can return
go c.serve(connCtx)
}
}
可以看到这是一个for的死循环,主协程accept连接,存在变量rw里面。然后用这个变量的值新建一个连接,另起一个go协程去处理刚才accept的请求,主协程继续accept新的请求,并继续建立连接。
serve函数太长,不方便全文粘贴,只复制比较关键位置的代码:
func (c *conn) serve(ctx context.Context) {
c.remoteAddr = c.rwc.RemoteAddr().String()
ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
defer func() {
if err := recover(); err != nil && err != ErrAbortHandler {
const size = 64 << 10
buf := make([]byte, size)
buf = buf[:runtime.Stack(buf, false)]
c.server.logf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf)
}
if !c.hijacked() {
c.close()
c.setState(c.rwc, StateClosed)
}
}()
//
............
serverHandler{c.server}.ServeHTTP(w, w.req)
}
到了serverHandler{c.server}.ServeHTTP(w, w.req)这一行,还记得前面调用http库的ListenAndServe函数时,传入的参数:地址,和我们的engine结构体(他实现了ServeHTTP这个interface)。那这个server的Handler也就是我们之前传进去的engine,这样绕了一圈以后就回到了我们自定义的ServeHTTP方法。这里还有一点,如果我们在new一个Server结构体的时候如果不传这个Handler,那这个服务器就会调用http库自带的ServeHTTP方法。通过讲这个Handler成员定义成Interface,go给了开发人员自己实现他们响应http请求的权利,非常的灵活。
绕了一圈,又回到了engine里面的serveHTTP请求,来看看具体做了啥。
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)
}
首先从之前的的pool里面取一个Context,然后初始化,接着调用handleHTTPRequest这个函数处理请求,并将相关信息写入这个Cntext,最后处理完以后,把这个Context放回engine的pool。
先看Context这个结构体,也就是一个请求的上下文,翻译成背景会不会更好点。存储了当前请求的相关信息,主要的包括:Request、处理函数、路径等,这里Keys可以用来存储当前请求的一些信息,
type Context struct {
writermem responseWriter
Request *http.Request
Writer ResponseWriter
Params Params
handlers HandlersChain
index int8
fullPath string
engine *Engine
// Keys is a key/value pair exclusively for the context of each request.
Keys map[string]interface{}
// Errors is a list of errors attached to all the handlers/middlewares who used this context.
Errors errorMsgs
// Accepted defines a list of manually accepted formats for content negotiation.
Accepted []string
// queryCache use url.ParseQuery cached the param query result from c.Request.URL.Query()
queryCache url.Values
// formCache use url.ParseQuery cached PostForm contains the parsed form data from POST, PATCH,
// or PUT body parameters.
formCache url.Values
}
可以看到这个Context实现了Set和Get方法,类似golang自带的context包,可以保存key value键值对,并在后续的操作中读取。
// Set is used to store a new key/value pair exclusively for this context.
// It also lazy initializes c.Keys if it was not used previously.
func (c *Context) Set(key string, value interface{}) {
if c.Keys == nil {
c.Keys = make(map[string]interface{})
}
c.Keys[key] = value
}
// Get returns the value for the given key, ie: (value, true).
// If the value does not exists it returns (nil, false)
func (c *Context) Get(key string) (value interface{}, exists bool) {
value, exists = c.Keys[key]
return
}
再看handleHTTPRequest做了什么。省略了前后代码,只看核心部分。在trees成员里面找到请求里对应注册的路
func (engine *Engine) handleHTTPRequest(c *Context) {
.....
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
}
if httpMethod != "CONNECT" && rPath != "/" {
if value.tsr && engine.RedirectTrailingSlash {
redirectTrailingSlash(c)
return
}
if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
return
}
}
break
}
//
..............
}
由以后,给Context的handlers、Params、fullPath等赋值,然后调用了Next的方法。
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}
然后遍历执行Handler里面的每一个方法。这里为啥index++,可以看到
func (c *Context) reset() {
c.Writer = &c.writermem
c.Params = c.Params[0:0]
c.handlers = nil
c.index = -1
c.fullPath = ""
c.Keys = nil
c.Errors = c.Errors[0:0]
c.Accepted = nil
c.queryCache = nil
c.formCache = nil
}
Context在reset的时候index是-1,++以后才能是从第一个方法开始执行。Context包自带了各种回包格式处理函数,可以写进http响应体。
至此整个自定义的ServeHTTP的响应完成,又回到了http库的响应函数里面,还记得之前的那个go c.serve(connCtx) 协程,当前http请求处理完成。
后续会介绍gin的路由树,以及与http自带路由树的优劣对比。