go-gin源码分析

gin的http服务启动

test := gin.Default()
test.Run()

上面简简单单的两行代码,就能开启一个基于gin框架的http服务
下面复杂的这么多行代码也是在启动一个基于gin的http服务。
了解过GOhttp包的应该很好理解下面的代码

//返回一个gin的engine对象
func InitRouter() *gin.Engine{...}
//构建一个server对象,传入engine作为handler
s := &http.Server{
        Addr:           fmt.Sprintf(":%d", setting.ServerSetting.HttpPort),
        Handler:        router,
        ReadTimeout:    setting.ServerSetting.ReadTimeout,
        WriteTimeout:   setting.ServerSetting.WriteTimeout,
        MaxHeaderBytes: 1 << 20,
    }
//将http作为协程开启
    go func() {
        if err := s.ListenAndServe(); err != nil {
            log.Printf("Listen: %s\n", err)
        }
    }()
  //监听系统的信号(crtl+c)
    quit := make(chan os.Signal)
    signal.Notify(quit, os.Interrupt)
    <- quit

    log.Println("Shutdown Server ...")
//利用contex来关闭http服务(防止服务意外关闭所以留5s的扫尾时间)
    ctx, cancel := context.WithTimeout(context.Background(), 5 * time.Second)
    defer cancel()
    if err := s.Shutdown(ctx); err != nil {
        log.Fatal("Server Shutdown:", err)
    }

    log.Println("Server exiting")

Gin默认的启动方式

我们先来看看两行代码的http服务是怎么开启的

func Default() *Engine {
//检验版本
    debugPrintWARNINGDefault()
//生成一个engine对象
    engine := New()
//默认使用Logger,Recovery中间件
    engine.Use(Logger(), Recovery())
    return engine
}
//参数就是返回一个Engine对象(如果一个函数太长,先看参数和返回值了解大概是干什么的)
func New() *Engine {
    debugPrintWARNINGNew()
    engine := &Engine{
        //路由组
        // 给框架实例绑定上一个路由组
        RouterGroup: RouterGroup{
            // engine.Use 注册的中间方法到这里
            Handlers: nil,
            basePath: "/",
            // 是否是路由根节点
            root:     true,
        },
        FuncMap:                template.FuncMap{},
        RedirectTrailingSlash:  true,
        RedirectFixedPath:      false,
        HandleMethodNotAllowed: false,
        ForwardedByClientIP:    true,
        AppEngine:              defaultAppEngine,
        UseRawPath:             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
}

还有Run方法

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

    address := resolveAddress(addr)
    debugPrint("Listening and serving HTTP on %s\n", address)
    // 执行http包的ListenAndServe方法 启动路由
    // engine实现了http.Handler接口 所以在这里作为参数传参进去
    // 后面我们再看engine.ServeHTTP的具体逻
    err = http.ListenAndServe(address, engine)
    return
}

了解了这个其实就很容易理解第二个是启动gin http服务的。
那么engine为什么能作为handler传入server呢,那肯定是实现了ServeHTTP方法即实现了handler接口

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    // 获取一个上下文实例
    // 从实例池获取 性能高
    c := engine.pool.Get().(*Context)
    // 重置获取到的上下文实例的http.ResponseWriter
    c.writermem.reset(w)
    // 重置获取到的上下文实例*http.Request
    c.Request = req
    // 重置获取到的上下文实例的其他属性
    c.reset()
      //这里重置是应为从对象池中拿出来的contex可能具有脏数据
      //(也就是可能是其他请求的contex,有兴趣可以试着注释掉这里的重置)
      //看看会发生什么
    // 实际处理请求的地方
    // 传递当前的上下文
    engine.handleHTTPRequest(c)
    //归还上下文实例
    engine.pool.Put(c)
}

果然 engine实现了ServeHTTP方法那么在处理请求时,也是主要调用了这个方法。
接下来就好好分析engine以及engine的这个ServeHTTP方法做了什么事

engine

type Engine struct {
//这里的组合代表engine也实现了RouterGroup(就当作继承吧)
    RouterGroup  //路由组
       ....//去掉一些不太重要的结构
    pool             sync.Pool//contex的对象池
    trees            methodTrees//handler树
}

type methodTrees []methodTree

type methodTree struct {
    method string
    root   *node
}

type node struct {
    path      string
    indices   string
    children  []*node //子节点
    handlers  HandlersChain //handler链
    priority  uint32
    nType     nodeType
    maxParams uint8
    wildChild bool
}

type RouterGroup struct {
    Handlers HandlersChain
    basePath string
    engine   *Engine
    root     bool
}

type HandlerFunc func(*Context) //Contex也算是gin的核心了,稍后探索
type HandlersChain []HandlerFunc

这些结构稍后再探索,先从一个gin的流程开始

gin注册路由

路由其实分为两个部分,一个是路由设置部分,一个是路由匹配部分。
路由其实并不仅仅是url,还包括HTTP的请求方法,而实现一个REST风格的http请求,需要支持REST支持的方法,比如GET,PUT,POST,DELETE,OPTION等。

r.GET("/auth", api.GetAuth)
r.Handle(http方法,path,handler)

而gin中注册路由的方式就是调用对应的http方法,或是使用r.Handle注册这里的r是engine

// GET is a shortcut for router.Handle("GET", path, handle).
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
    //向路由组注册路由
    return group.handle("GET", relativePath, handlers)
}
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
    absolutePath := group.calculateAbsolutePath(relativePath)
    //合并中间件得handle和路由的handle
    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)
    if finalSize >= int(abortIndex) {
        panic("too many handlers")
    }
    mergedHandlers := make(HandlersChain, finalSize)
//注意这两个copy,是RouterGroup的handler在用户自定义handler前面。
    copy(mergedHandlers, group.Handlers)
    copy(mergedHandlers[len(group.Handlers):], handlers)
    return mergedHandlers
}
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)
    // 检查有没有对应method集合的路由
    //所以这里是每一个方法一个树
    root := engine.trees.get(method)
    if root == nil {
        // 没有 创建一个新的路由节点
        root = new(node)
        root.fullPath = "/"
        // 添加该method的路由tree到当前的路由到路由树里
        engine.trees = append(engine.trees, methodTree{method: method, root: root})
    }
    root.addRoute(path, handlers)
}

可能这里有点晕,但是只要记住handlers或者handlerschain都是func(*Contex)这个方法,而这个Contex里面有着GO原生http服务处理所有的的东西就好了,把它看成加强版的处理业务逻辑的对象

所以上述的这些函数就把路由与对应的handlers设置好了。

那么来看看路由是如何匹配的

路由匹配

还记得前面engine实现的ServeHTTP方法吗

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    // 获取一个上下文实例
    // 从实例池获取 性能高
    c := engine.pool.Get().(*Context)
    // 重置获取到的上下文实例的http.ResponseWriter
    c.writermem.reset(w)
    // 重置获取到的上下文实例*http.Request
    c.Request = req
    // 重置获取到的上下文实例的其他属性
    c.reset()
      //这里重置是应为从对象池中拿出来的contex可能具有脏数据
      //(也就是可能是其他请求的contex,有兴趣可以试着注释掉这里的重置)
      //看看会发生什么
    // 实际处理请求的地方
    // 传递当前的上下文
    engine.handleHTTPRequest(c)
    //归还上下文实例
    engine.pool.Put(c)
}

真正处理的就是handleHTTPRequest

func (engine *Engine) handleHTTPRequest(c *Context) {
    httpMethod := c.Request.Method
    rPath := c.Request.URL.Path
    unescape := false
    if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
        rPath = c.Request.URL.RawPath
        unescape = engine.UnescapePathValues
    }
    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++ {
        // 这里寻找当前请求method的路由树节点
        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 {
            // 把找到的handles赋值给上下文
            c.handlers = value.handlers
            // 把找到的入参赋值给上下文
            c.Params = value.params
            c.fullPath = value.fullPath
            // 执行handle
            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
    }

    if engine.HandleMethodNotAllowed {
        for _, tree := range engine.trees {
            if tree.method == httpMethod {
                continue
            }
            if value := tree.root.getValue(rPath, nil, unescape); value.handlers != nil {
                c.handlers = engine.allNoMethod
                serveError(c, http.StatusMethodNotAllowed, default405Body)
                return
            }
        }
    }
    c.handlers = engine.allNoRoute
    serveError(c, http.StatusNotFound, default404Body)
}

这里就通过getValue找到了对应的路由进行处理

gin的中间件

我们看到tree中路由对应的是HandlersChain,实际就是[]HandlerFunc,所以一个路由,实际上会对应多个handlers。

所以是可以一个路由拥有多个handler的。这里的handler是怎么来的呢?

每个路由的handler有几个来源,第一个来源是在engine.GET的时候调用增加的。第二个来源是RouterGroup.GET的时候增加的(而设置的方式上面已经说过了),而且group的handler高于自定义的handler(告诉我们中间件的回调要先于用户定义的路径处理函数。),这里自定义的handler可以是多个。

router.GET("/before", MiddleWare(), func(c *gin.Context) {
  request := c.MustGet("request").(string)
  c.JSON(http.StatusOK, gin.H{
    "middile_request": request,
  })
})

func MiddleWare() gin.HandlerFunc {
    return func(c *gin.Context) {
        fmt.Println("before middleware")
        c.Set("request", "clinet_request")
        c.Next()
        fmt.Println("before middleware")
    }
}

第三种方法是使用Use增加中间件的方式:
apiv1.Use(jwt.JWT()),apiv1 := r.Group("/api/v1")这里的apiv1是一个路由组,gin一般使用一个use来载入一个中间件

func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
    group.Handlers = append(group.Handlers, middleware...)
    return group.returnObj()
}

这里会把这个中间件(实际上也是一个handler)存放到routerRroup上。所以中间件是属于groupHandlers的。
上文说过handleHTTPRequest将对应路由的handler赋给了当前请求的上下文,并且调用next()函数执行。

func (c *Context) Next() {
    // 上下文处理之后c.index被执为-1
    c.index++
    for c.index < int8(len(c.handlers)) {
        // 遍历执行所有handle(其实就是中间件+路由handle)
        c.handlers[c.index](c)
        c.index++
    }
}

之前也提到过,context是从对象池里取出来的,所以可能带有脏数据,因此要重置

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
}

这里的Next方法没有使用局部变量去遍历计数handlers的,它使用了和Context的成员变量index。这样就可以保证某些情况下Next()函数不会触发任何handler的调用。

这里还有个细节,既然这里是调用c.Next()来交出当前Contex的控制权,并执行接下来的hanlder所以c.Next就可以视作是后面所有的handler都执行了。那么整个handler执行和返回的过程就可以看作一个栈,先执行的最后返回。

好了说到这里,,整个gin的流程应该已经明了了t

接下来探索一下前文提过多次的Context

Context

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{}
      ......
}

可以看到Context里集成RequestResponseWriter,就是说整个业务逻辑处理的参数数据都封装在这个Context里了
里面实现了几乎所有的http里处理的功能函数(bind(获取参数),render输出参数)

附上一份gin的源码(自己学习的时候写的,有比较详细的注释)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容