Gin是一个采用Go语言实现的HTTP web框架,提供了类似 Martini 的API,但是性能远强于Martini,峰值性能是Martini的40倍。如果我们的项目需要高性能,毫无疑问采用Gin。
Gin官网列举了该项目的8个如下关键特性:
下面的Demo程序来自官方文档,构建并运行该程序,然后在浏览器输入 http://localhost:8080/ping,显示字符串{"message":"pong"}。
package main
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(, gin.H{
"message": "pong",
})
})
r.Run() // listen and serve on 0.0.0.0:8080
}
可以看到,使用Gin框架启动一个web服务非常简洁,只需三步操作:
gin.Default()创建一个缺省的Engine对象。在Gin框架中,所有HTTP方法的处理函数需要在对应的Method上注册。即r.Get注册Get请求上的处理函数。HTTP协议定义了9种请求方法。最常用的方法GET、POST、PUT和DELETE分别对应查、增、改和删除操作,此外Gin还提供了Any接口,可以一次性将全部HTTP方法绑定到一个路由上。
返回内容包含3部分,其中状态码(code)和消息(message)必选,数据(data)为非必选,用于承载额外的业务数据。如果没有额外数据需要返回,可忽略data字段。上述示例中,code为200,message为pong。
在前面的示例程序中,通过gin.Default创建一个Engine对象,实际上该方法只做了一个简单的包装,内部调用的是New函数。与此同时,为engine.pool.New设置初始化函数,供engine.allocateContext创建上下文对象,详细分析见下文。
func New() *Engine {
debugPrintWARNINGNew()
engine := &Engine{
RouterGroup: RouterGroup{
//... Initialize the fields of RouterGroup
},
//... Initialize the remaining fields
}
engine.RouterGroup.engine = engine // Save the pointer of the engine in RouterGroup
engine.pool.New = func() any {
return engine.allocateContext()
}
return engine
}
结构体RouterGroup以嵌入形式存在于Engine中,因此Engine继承了RouterGroup实现的方法。
type RouterGroup struct {
Handlers HandlersChain // Processing functions of the group itself
basePath string // Associated base path
engine *Engine // Save the associated engine object
root bool // root flag, only the one created by default in Engine is true
}
每个RouterGroup含有一个basePath,即URL参数,对于Engine来说,它的basePath是/。Handlers为要执行的函数集合,匹配上basePath的URL会执行Handlers中的所有处理函数,当然中间件函数也在Handlers中。
创建Engine对象时,Handlers设置为空nil,我们可以使用Use方法往里面注册处理函数。
RouterGroup的handle方法是所有HTTP注册回调中最后的入口函数。
r.Get方法调用的是RouterGroup的Get方法,该方法会调用handle注册处理函数。
r.GET("/ping", func(c *gin.Context) {
c.JSON(, gin.H{
"message": "pong",
})
})
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodGet, relativePath, handlers)
}
RouterGroup的handle方法实现如下,主要有三点。一是计算绝对的path,即完整的URL。二是处理函数合并。三是将路由添加到engine中。
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()
}
combineHandlers方法核心就是干一件事:处理函数合并。申请一个新的函数切片,并按finalSize大小进行初始,然后将group中的handler和新注册的handler一起合并到mergedHandlers返回。
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
finalSize := len(group.Handlers) + len(handlers)
assert1(finalSize < int(abortIndex), "too many handlers")
mergedHandlers := make(HandlersChain, finalSize)
copy(mergedHandlers, group.Handlers)
copy(mergedHandlers[len(group.Handlers):], handlers)
return mergedHandlers
}
注意通过Radix Tree实现加速,是引入httprouter库实现的,本文聚集于应用层面,不深入分析基数树的底层实现。
在结构体Engine中,trees是一个核心字段,用于维护基数树的引用,该字段本质是methodTree结构体切片。
type methodTrees []methodTree
type methodTree struct {
method string
root *node
}
Engine为每种HTTP请求方法维护着一个基数树。基数树的头对应methodTree结构的root字段,请求方法对应methodTree中的method字段。
Engine的addRoute方法将路由处理函数添加到Engine的trees中,结合下面代码,可见第一步就是从engine.trees中获取到对应method的基数树。如果基数树不存在,则说明是首次添加对应的method,将创建一个基数树添加到engine.trees中。
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
//... Omit some code
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)
//... Omit some code
}
获取到基数树后,则调用它的addRoute方法将路由和处理函数注册到基数树上。具体来说,创建一个新的节点,节点的path和handler分别为addRoute的入参。若尝试注册已存在的地址,addRoute 将直接抛出 panic 异常。
当处理HTTP请求时,调用getValue方法通过路径在基数树中找到对应节点的值。
使用RouterGroup的Use方法可以添加中间件处理函数。Engine默认实例化函数中,使用Use方法默认注册了日志和异常处理中间件函数,代码如下。
func Default() *Engine {
debugPrintWARNINGDefault() // Output log
engine := New() // Create object
engine.Use(Logger(), Recovery()) // Import middleware processing functions
return engine
}
Logger函数的返回值也是一个函数,函数签名为 type HandlerFunc func(*Context),默认将程序日志输出到标准输出中。Recovery函数处理程序引发的panic问题。
func Logger() HandlerFunc {
return LoggerWithConfig(LoggerConfig{})
}
尽管Engine中内嵌的RouterGroup实现了Use方法,但Engine本身也实现了Use方法,实现代码如下。
func (engine *Engine) Use(middleware...HandlerFunc) IRoutes {
engine.RouterGroup.Use(middleware...)
engine.rebuild404Handlers()
engine.rebuild405Handlers()
return engine
}
RouterGroup的Use方法实现逻辑非常简单,就是将处理函数添加到自己的Handlers中。
func (group *RouterGroup) Use(middleware...HandlerFunc) IRoutes {
group.Handlers = append(group.Handlers, middleware...)
return group.returnObj()
}
调用Engine的Run方法即运行它,Run方法接收可变参数。我们也可以什么都不传,像本文开头的例子,默认监听在0.0.0.0:8080上。
func (engine *Engine) Run(addr...string) (err error) {
//... Omit some code
address := resolveAddress(addr) // Parse the address, the default address is 0.0.0.0:8080
debugPrint("Listening and serving HTTP on %s\n", address)
err = http.ListenAndServe(address, engine.Handler())
return
}
Run内部实现主要完成两件事。一是解析监听地址,二是启动服务。插入的地址为字符串类型,为了实现参数可选特性,采用可变长参数设计。封装resolveAddress 方法负责处理地址解析工作。启动服务使用标准库net/http包中的ListenAndServer方法,该方法接受两个参数,参数1为监听地址,参数2为实现了Handler接口的实例。
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
因为Engine实现了ServeHTTP接口,所以它可以传递给ListenAndServe。当有新连接时,ListenAndServe负责接受并建立连接,接收到连接上的数据后,调用处理器的ServeHTTP方法完成请求处理。
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)
}
ServeHTTP方法参数w用于处理HTTP请求响应,参数req用于读取本次处理的数据。整个ServeHTTP方法执行下面四个操作:
下面是handleHTTPRequest的关键代码,主要处理2点事情。一是根据请求URL从基数树中获取到对应的处理函数,将其赋值给Context对象;二是调用Next方法执行处理函数handlers,最后将请求返回值写入Context中的responseWriter响应对象。
func (engine *Engine) handleHTTPRequest(c *Context) {
//... Omit some code
t := engine.trees
for i, tl := , 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, c.skippedNodes, unescape)
//... Omit some code
if value.handlers!= nil {
c.handlers = value.handlers
c.fullPath = value.fullPath
c.Next()
c.writermem.WriteHeaderNow()
return
}
//... Omit some code
}
//... Omit some code
}
Context是Gin框架中非常重要的组成部分,HTTP请求处理时所有的相关数据都存储在Context结构体中。通过前面Engine的ServeHTTP逻辑可以看到,并不是直接创建context对象,而是从Engine的对象池中获取,即engine.pool的Get方法。获取到的对象需要先进行重置初始化,使用完毕后通过Put放回到pool中。
Engine对象中的pool类型是sync.Pool,它是标准库中提供的并发安全对象池,主要提供了Get、Put和New方法。
New()方法实现如下,调用Engine的allocateContext方法创建Context对象。
func New() *Engine {
//... Omit other code
engine.pool.New = func() any {
return engine.allocateContext()
}
return engine
}
构建Context对象时采用了切片容量预分配策略,避免频繁的内存申请开销,下面代码中的v和skippedNodes都是提前预分配大小。
func (engine *Engine) allocateContext() *Context {
v := make(Params, , engine.maxParams)
skippedNodes := make([]skippedNode, , engine.maxSections)
return &Context{engine: engine, params: &v, skippedNodes: &skippedNodes}
}
调用Context的Next方法才真正执行handlers逻辑,代码实现非常简洁巧妙,通过下标访问处理c.handlers中的每个handler,c.index初始值为-1,首次c.index++为0,执行第一个处理函数。
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}
之所以采用下标遍历handlers,有一个重要的原因是当某个handler执行中触发panic,可以通过中间件的recover机制捕获异常后,重新调用Next()方法继续执行后续处理函数,从而确保单个handler故障不会中断整个处理链。
在Gin框架中,某个请求的处理函数产生panic,程序不会直接崩溃,框架做了异常处理,具体来说就是输出错误信息并保持服务继续运行,这种处理机制与Lua框架中通过xpcall执行消息处理函数的容错策略类似。
这里我们暂不深究处理细节,只关注核心操作。customRecoveryWithWriter函数返回了一个匿名函数,在匿名函数中通过defer注册了一个匿名函数,主要逻辑有以下3点:
func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
//... Omit other code
return func(c *Context) {
defer func() {
if err := recover(); err!= nil {
//... Error handling code
}
}()
c.Next() // Execute the next handler
}
}