Golang 框架 gin运行源码分析
【导读】gin框架是go语言web框架中非常流行的框架之一,本文详细解读了gin框架的优点与其实现。
解析gin框架中数据流转原理
分析流程
我们已经了解了http包中server的启动方法, 现在我们分析gin处理流程
gin框架号称 路由提速了40倍, 所以,到底他是哪里快呢?
我们还是看下他启动服务时做哪些事情
package main
import "github.com/gin-gonic/gin"
func main() {
#生成gin实例
r := gin.Default()
#注册路由关系,定义具体的路由处理方法
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
#启动服务
r.Run() // listen and serve on 0.0.0.0:8080
}
和我们刚才看到的http_server
非常像, 但是路由管理器这块比较强大
较高级的语法糖, 可以直接指定HTTP方法名称, 内部封装json响应实现。
我们继续从启动方法r.Run()
入手, 看下他的内部实现逻辑
#gin.go
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方法
err = http.ListenAndServe(address, engine)
return
}
我们可以发现, 这里仍然使用的是http包的ListenAndServe
方法, 和我们上一章调用类似,主要区别是其第二个参数, 这里是gin框架生成gin实例。
并且我们知道, 第二个参数的主要意义是, 关联路由管器对象, 即gin框架承接了url和处理函数的路由管理。其他http请求这一套, 继续复用原生包的http接口
我们截取下http包的server.go
的代码
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
#获取路由管理对象, 这里即是我们调用`ListenAndServe`的第二个参数
handler := sh.srv.Handler
#如果没有设置,则使用默认的defaultServerMux
if handler == nil {
handler = DefaultServeMux
}
if req.RequestURI == "*" && req.Method == "OPTIONS" {
handler = globalOptionsHandler{}
}
#进入路由管理对象的ServerHttp方法中,进行路由分发
handler.ServeHTTP(rw, req)
}
从handler.ServerHTTP
方法开始,gin框架开始接管http请求
因此我们看下gin.go中的ServerHTTP方法
// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
#为减少gc重复回收, 这里使用sync.pool管理自定义Context对象
c := engine.pool.Get().(*Context)
c.writermem.reset(w)
c.Request = req
c.reset()gg
#分发处理请求
engine.handleHTTPRequest(c)
#将Context对象放回
engine.pool.Put(c)
}
我们看下上面代码, 主要做以下事情
-
为减少gc重复回收, 这里使用 sync.pool
管理自定义Context对象 -
将请求reqeust数据copy到Context对象中, 通过Context进行管理 -
调用 engine.handleHTTPRequest
进行路由分发 -
在这里引入自定义的Context对象, 其主要是用来管理数据流转过程时的,上下文数据, 比如response, request, 请求参数params,路径fullpath, 查询缓存, 错误管理, 主要的目的是:避免重复复制数据。 -
保证数据的一致性。这是gin最重要的数据结构体
我们着重看下 engine.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++ {
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 {
#更新Context对象属性,将路由地址管理的多个路由函数都交给Context管理
c.handlers = value.handlers
c.Params = value.params
c.fullPath = value.fullPath
#递归的执行关联的handler方法
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)
}
上述代码完成完整路由,进行数据的最终响应, 我们把核心代码摘出来。
#根据路径,请求参数,找到对应的 路由处理函数, 和我们上一章节实现类似
value := root.getValue(rPath, c.Params, unescape)
#递归的执行关联的handler方法
c.Next()
我们先看 root.getValue
方法, 其主要进行 路由操作,这里是采用的基树Radix_tree
算法,实现较复杂,不再这里详细展开了。
我们再看下c.Next()
方法,这个方法的核心,主要是方便接入中间件(Middleware),使得代码模块化操作。
我们看下Next的具体实现
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
#执行关联的中间件方法或者 实际路由处理函数
c.handlers[c.index](c)
c.index++
}
}
描述相对抽象, 我们新建一个demo例子
import (
"fmt"
"github.com/gin-gonic/gin"
"log"
"time"
)
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
t := time.Now()
//请求处理前 Set example variable
c.Set("example", "12345")
//请求下一个中间件
c.Next()
//请求处理之后
latency := time.Since(t)
log.Print(latency)
// access the status we are sending
status := c.Writer.Status()
log.Println(status)
}
}
func main() {
r := gin.New()
//引入Logger()中间件
r.Use(Logger())
r.GET("/test", func(c *gin.Context) {
example := c.MustGet("example").(string)
//打印关联的全部处理路由函数
fmt.Println(c.HandlerNames())
// it would print: "12345"
log.Println(example)
})
// Listen and serve on 0.0.0.0:8080
r.Run(":8080")
}
上面例子中, 我们引入一个Logger中间件, 在路由函数中打印全部路由函数名称,输出如下
[main.Logger.func1 main.main.func1]
即能看到,这里将我们新加入的Logger中间件,转换成了句柄函数,即/testURI对应的路由函数有两个, 这两个会按照先后顺序, 依次执行。
即先执行 main.Logger.func1
, 后执行 main.main.func1
, 结合我们上面的Next方法实现, 我们就能清楚的知道,其调用关系
实现了Next方法的伪代码,加深理解:
进入Next
index++= 0
调用handler方法,即Logger方法(如果index<总handler数)
进入Logger方法
//logger业务逻辑
进入Next方法
//index++ = 1
这里还可以继续加入其他中间件
//进入fun1方法
//从fun1方法离开
//index++ = 2
离开Next方法
//logger业务逻辑处理
离开Logger方法
index++ = 3
离开Next方法
逻辑处理完毕
这里比较重要的概念是, 处理函数有先后执行关系, 并且处理函数可以通过调用Abort方法, 提前返回,不用递归调用到实际处理函数。
这些中间件,可以方便的使我们的业务代码接入权限校验auth,日志管理等其他功能模块。
总结
核心亮点:路由管理对象的实现+ 中间件的实现原理
1905.github.io/golang%E5%BC%80%E5%8F%91/2019/08/14/golang-gin-webserver/
- EOF -
1、
2、
3、
Go 开发大全
参与维护一个非常全面的Go开源技术资源库。日常分享 Go, 云原生、k8s、Docker和微服务方面的技术文章和行业动态。
关注后获取
回复 Go 获取6万star的Go资源库
分享、点赞和在看
支持我们分享更多好文章,谢谢!