「Go 框架」通过分析 gin、beego 框架, 读懂 web 框架对 http 请求的处理流程

大家好,我是渔夫子。本号新推出「Go 工具箱」系列,意在给大家分享使用 go 语言编写的、实用的、好玩的工具。同时了解其底层的实现原理,以便更深入地了解 Go 语言。
在实际工作中,大家一定会用到 go 的 web 框架。那么,你知道各框架是如何处理 http 请求的吗?今天就主流的 web 框架gin、beego框架以及 go 标准库net/http来总结一下 http 请求的流程。
一、标准库 net/http 的请求流程
首先,我们来看下 http 包是如何处理请求的。通过以下代码我们就能启动一个 http 服务,并处理请求:
当我们输入http://localhost:8000/的时候,就会执行到HomeHandler的ServeHTTP方法,并返回Hello World。
那这里为什么要给HomeHandler定义ServeHTTP方法,或者说为什么会执行到ServeHTTP方法中呢?
我们顺着http.ListenAndServe方法的定义:
发现第二个参数是个Handler类型,而Handler是一个定义了ServeHTTP方法的接口类型:
似乎有了一点点关联,HomeHandler类型也实现了ServeHTTP方法。但我们在 main 函数中调用http.ListenAndServe(":8000", nil)的时候第二个参数传递的是nil,那HomeHandler里的ServeHTTP方法又是如何被找到的呢?
我们接着再顺着源码一层一层的找下去可以发现,在/src/net/http/server.go的第 1930 行有这么一段代码:
有个serverHandler结构体,包装了c.server。这里的c是建立的 http 连接,而c.server就是在http.ListenAndServe(":8000", nil)函数中创建的server对象:
server中的 Handler 就是http.ListenAndServe(":8000", nil)传递进来的nil。
好,我们进入 serverHandler{c.server}.ServeHTTP(w, w.req)函数中再次查看,就可以发现如下代码:
/src/net/http/server.go的第 2859 行到 2862 行,就是获取到server中的Handler,如果是nil,则使用默认的DefaultServeMux,然后调用了hander.ServeHTTP方法。
继续再看 DefaultServeMux 中的 ServeHTTP 方法,在 /src/net/http/server.go 中的第 2416 行,发现有一行 h, _ := mux.Handler(r) 和 h.ServeHTTP 方法的调用。这就是通过请求的路径查找到对应的 handler,然后调用该handler的ServeHTTP方法。在开始的实例中,就是我们的HomeHandler的ServeHTTP方法。
也就是说ServeHTTP方法是net/http包中规定好了要调用的,所以每一个页面处理函数都必须实现 ServeHTTP 方法。
二、gin 框架的 http 的请求流程
gin 框架对 http 的处理流程本质上都是基于 go 标准包 net/http 的处理流程的。 下面我们看下 gin 框架是如何基于 net/http 实现对一个请求处理的。首先我们看通过 gin 框架是如何启动 http 服务的:
我们查看engine.Run函数的源码,发现也是通过net/http包启动的http服务。如下:
函数较短,在第 11 行,通过http.ListenAndServe(address, engine.Handler())函数启动的http服务。和第一节中的通过go的标准库net/http启动的服务方式一样,只不过第二个参数不是nil,而是engine.Handler()。
我们继续查看engine.Handler()函数的源码,发现该函数返回的是一个http.Handler类型。在源代码中,返回的是engine对象。这里暂且不讨论使用http2的情况。也就是说engine实现了http.Handler接口,即实现了http.Handler接口中的ServeHTTP函数。
我们再查看Engine结构体中实现的方法,发现有ServeHTTP函数的实现,如下:
这里我们主要看第 8 行的engine.handleHTTPRequest(c)函数,代码如下:
主要看第 14 行的代码部分,根据请求的路径查找路由,找到了对应的路由,从路由中获取该路径对应的处理函数,赋值给该框架自定义的上下文对象c.handlers,然后执行c.Next()函数。
c.Next()函数实际上就是循环c.handlers,源码如下:
而c.handlers是一个HandlersChain类型,如下:
HandlersChain类型本质上是一个HandlerFunc数组,而HandlerFunc类型的定义如下:
这个函数类型是不是就是在注册路由engine.GET("/", HomeHandler)时HomeHandler的类型呢?如下是我们注册路由以及定义HomeHandler的代码:
这样就形成了一个处理流程的闭环。我们总结下 gin 框架对 http 请求的处理流程。
首先,通过 gin.New()创建一个 Engine 结构体实例,该 Engine 结构体实现了 net/http 包中的 http.Handler 接口中的 ServeHTTP 方法。
通过 engine.Run 函数启动服务。本质上也是通过 net/http 包中的 http.ListenAndServe 方法启动服务的,只不过是是将 engine 作为服务接收请求的默认 handler。即 Engine.ServeHTTP 方法。
在 Engine 结构体的 ServeHTTP 方法中,通过路由查找找到该次请求的对应路由,然后执行对应的路由执行函数。即 func(ctx *gin.Context)类型的路由。
以下是 gin 框架处理 http 请求的全景图:
三、beego 框架的 http 请求处理流程
beego 框架启动 http 服务并监听处理 http 请求本质上也是使用了标准包 net/http 中的方法。和 gin 框架不同的是,beego 直接使用 net/http 包中的 Server 对象进行启动,而并没有使用 http.ListenAndServe 方法。但本质上是一样的,http.ListenAndServe 方法的底层是也调用了 net/http 包中的 Server 对象启动的服务。
首先我们看下 beego 框架启动 http 服务的过程:
在上述代码中,我们注册了一个 /home 路由,然后再8000端口上启动了 http 服务。接下来我们看下web.Run(":8000")的内部实现:
在该函数中,调用了BeeApp的Run方法。 这里你会发现有两次BeeApp.Run调用,为什么要调用两次呢?这里其实不是一个 bug。我们进BeeApp.Run函数就可以知道,其实Run方法运行后就阻塞了,不会进行最后的BeeApp.Run("")调用,所以不会出现两次调用。如下在第 34 行时,实际上是通过通道的输出方式进行了阻塞(这里为进行说明,只列出了相关的代码):
我们再详细看下BeeApp实例。BeeApp是*HttpServer类型的实例,在导入包时,通过init函数进行的初始化。其定义如下:
我们看下 HttpServer 的结构体包含的主要字段如下:
有两个关键的字段,一个是http.Server类型的Server,这个就是用来启动并监听服务。看吧,万变不离其宗,最终启动和监听服务还是使用 go 标准包中的 net/http。
另外一个就是ControllerRegister类型的Handlers。这个字段就是用来管理路由和 http 请求的入口。我们看下ControllerRegister结构体的关键字段:
在ControllerRegister中关键的字段也有两个,一个是路由表routers,一个是进行路由匹配的FilterRouter类型。
我们再来看ControllerRegister结构体实现的方法中有一个是ServeHTTP方法,说明是实现了标准表 net/http 中的http.Handler接口,源码如下:
其中第 8 行的 p.chainRoot.filter(ctx, p.getUrlPath(ctx), preFilterParams)就是路由匹配的过程。实际的路由匹配和执行过程实际上是在ControllerRegister的serveHttp方法中,这里注意和http.Handler接口的ServerHTTP方法的首字母的大小写的区别。 serveHttp方法是在初始化chainRoot�对象时指定的过滤函数,在第 13 行的newFilterRouter的第二个参数就是具体的路由匹配函数,如下:
最后,我们再看下路由注册的过程。路由注册有三种方式,这里我们只看其中的一种:用可执行函数进行注册,如下:
这里HomeHandler就是一个函数类型。我们随着web.Get的源码一路找下去,发现最终会返回一个ControllerInfo路由信息:
大家看,第 6 行的f就是HomeHandler这个函数,给路由的runFunction进行了赋值。 在路由匹配阶段,找到了对应的路由信息后,就执行route.runFunction即可。
好了,beego 框架处理 http 请求的流程基本就是这样,具体的路由实现我们后续再单独起一篇文章介绍。如下是该框架处理 http 请求的一个全景图:
四、总结
通过以上两个流行的开源框架 gin 和 beego 以及 go 标准包 net/http 处理 http 请求的分析,可以得知所有的 web 框架启动 http 服务和处理 http 的流程都是基于 go 标准包 net/http 执行的。 其本质流程都都是通过net/http启动服务,然后调用handler中的ServeHTTP方法。而框架只要实现了 http.Handler 接口中的ServeHTTP方法,并作为 http 服务的默认入口,就可以在框架中的ServeHTTP方法中进行路由分发了。如下图:
---特别推荐---
特别推荐:一个专注 go 项目实战、项目中踩坑经验及避坑指南、各种好玩的 go 工具的公众号,「Go 学堂」,专注实用性,非常值得大家关注。关注送《100 个 go 常见的错误》pdf 文档。
版权声明: 本文为 InfoQ 作者【Go学堂】的原创文章。
原文链接:【http://xie.infoq.cn/article/5c121ce578fb3824f6dff69f2】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。









评论