源码阅读-gin

137 阅读6分钟

Gin is a HTTP web framework written in Go (Golang). It features a Martini-like API, but with performance up to 40 times faster than Martini. If you need smashing performance, get yourself some Gin.

QuickStart

package main

import (
  "net/http"

  "github.com/gin-gonic/gin"
)

func main() {
  r := gin.Default()
  r.GET("/ping", func(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{
      "message": "pong",
    })
  })
  
  r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}

这里我们使用 gin.Default() 来初始化一个 Engine

Engine 是框架的实例,它包含muxer、中间件和配置设置

func Default() *Engine {  
    debugPrintWARNINGDefault()  
    engine := New()  
    engine.Use(Logger(), Recovery())  
    return engine  
}

这里我们可以看出 gin.Default() 实际上是调用了 gin.New() 初始化一个原生的 Engine,然后调用 engine.Use() 绑定两个全局的中间件,Logger 用于打印日志,Recovery 用于防止 panic

RouterGroup

func main() {  
    r := gin.New()
    
    v1 := r.Group("/v1")  
    v1.GET("/index", func(c *gin.Context) {  
        c.String(http.StatusOK, "hello world")  
    })  

    user := v1.Group("/admin")  
    user.GET("/hello", func(c *gin.Context) {  
        c.String(http.StatusOK, "hello admin")  
    })  

    r.Run() 
}

通过创建路由组来返回一个路由组对象,使用路由组添加路由,可以节省填写相同路径前缀和中间件的步骤,实际运行结果如下

$ curl --location --request GET http://localhost:8080/v1/index
hello world%

$ curl --location --request GET http://localhost:8080/v1/admin/hello
hello admin%

无论是 GET 或者 POST 方法,都是调用了 RouterGroup.handle() 方法

func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {  
    return group.handle(http.MethodGet, relativePath, handlers)  
}

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()  
}

调用 Engine.addRouter() 将路由方法、路径、处理器切片存入路由树中

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)  
  
    // Update maxParams  
    if paramsCount := countParams(path); paramsCount > engine.maxParams {  
        engine.maxParams = paramsCount  
    }  
  
    if sectionsCount := countSections(path); sectionsCount > engine.maxSections {  
        engine.maxSections = sectionsCount  
    }  
}

目前业界 Server 端 API 接口的设计方式一般是遵循 RESTful 风格的规范,同时 gin 注册路由的时候,会根据不同的 Method 分别注册不同的路由树

root := engine.trees.get(method) 得到方法的根节点,如果为空,则新建一个路由树

root.addRoute(path, handlers) 具有给定处理器的节点添加到路径中

生成路由树: gin 路由树是一棵前缀树

一个路径,比如/p/blog,它被/正斜杠划分为多段,如果我们把其中的一段给抽象为一个类似于节点,然后通过指针把这几段连接起来,形成一个前缀树,如下图所示

image.png

middleware

ginServer.Use() 注册中间件,那么当前服务的所有请求都会被该中间件拦截处理,结合路由分组,gin 支持全局中间件,局部中间件和单个路由的中间件

func main() {  
    r := gin.New()  
    r.GET("/index", func(c *gin.Context) {  
        c.String(http.StatusOK, "hello world")  
    })  
    // 绑定全局中间件,但是上面一部分函数不会使用
    r.Use(gin.Logger())  
  
    v1 := r.Group("/v1")
    // 绑定局部中间件
    v1.Use(func(c *gin.Context) {  
        c.Set("version", "v1")  
        c.Next()  
    })  
    // 为单个路由绑定中间件  
    v1.GET("/admin", middleware1(), func(c *gin.Context) {  
        value, _ := c.Get("admin")  
        c.JSON(http.StatusOK, gin.H{  
            "name": value,  
            "msg": "hello",  
        })
        log.Println("GET response json......")
    })  
    r.Run(":8080")  
}  
  
func middleware1() gin.HandlerFunc {  
    return func(c *gin.Context) {
       log.Println("middleware 1 start......")
       if v, e := c.Get("version"); e == false || v != "v1" {
          c.Abort()
       }
       c.Set("admin", "admin001")
       c.Next()
       log.Println("middleware 1 end......")
    }
}

运行上面的程序,服务端会输出如下字符串

$ curl --location --request GET http://localhost:8080/v1/index 
middleware 1 start......
GET response json......
middleware 1 end......

从上面的程序其实可以看出来,gin 的中间件和普通处理函数并没有区别,都是 type HandlerFunc func(*Context) 类型

但是,在中间件中,需要通过 NextAbort 方法,控制 handler 执行顺序

//Next只能在中间件内部使用。
//它在调用处理程序内部执行链中的挂起处理程序。
func (c *Context) Next() {
    c.index++
    // 限制调用的嵌套层数
    for c.index < int8(len(c.handlers)) {
        c.handlers[c.index](c)
        c.index++
    }
}
const abortIndex int8 = math.MaxInt8 >> 1

func (c *Context) Abort() {
   c.index = abortIndex
}

这样可以把中间件的调用,理解为 洋葱模型

image.png

这也就是为什么开始运行的 demo,在执行 middleware1()log.Println("middleware 1 start......") 后,调用 c.NEXT() 执行 index++ 进入下一层,最后返回执行 log.Println("middleware 1 end......"),多嵌套几个中间件也是同样的道理

如果需要自己实现一个中间件的结构,

gin.Context

在中间件中,通过 gin.context 管理 HandlerFunc 的执行顺序,同时它还可以链式传递共享变量

gin.context的定义如下

type Context struct {
writermem responseWriter              //相应处理
Request   *http.Request               //请求报文的抽象
Writer    ResponseWriter              //响应报文的抽象

Params   Params                       //记录了url参数,是一组键值对
handlers HandlersChain                //定义一个 HandlerFunc 数组
index    int8                         //定义一个HandlerFunc数组的索引。next() / abort() 会用到
fullPath string                      //请求地址

engine       *Engine                 //gin 框架实例对象,gin.New()返回的就是一个 \*Engine 实例
params       *Params                 //URL 参数的切片
skippedNodes *[]skippedNode          //路由中配置的需要跳过的路径节点

mu sync.RWMutex                     //读写锁,用来保护Keys

Keys map[string]any                 //是专门针对每个请求的上下文的键/值对,Set()和Get()的数据就放在这里。

Errors errorMsgs                   //使用此上下文的所有处理程序或者中间件附带的错误列表

Accepted []string                  //自定义请求接收的内容类型格式

queryCache url.Values              //底层数据类型是 map[string][]string,使用url.ParseQuery()从c.Request.URL.Query()缓存了参数查询结果

formCache url.Values              //使用url.ParseQuery缓存的PostForm包含来自POST,PATCH或者PUT请求参数

sameSite http.SameSite           //允许服务器定义cookie属性。用来防止 CSRF 攻击和用户追踪
}

对官方context的实现

func (c *Context) Deadline() (deadline time.Time, ok bool) {
    // ContextWithFallback默认是false 一般都不会回退的。
    if !c.engine.ContextWithFallback || c.Request == nil || c.Request.Context() == nil {
        return
    }
    //如果允许回退,那么Context会直接回退为Request下的Context。
    return c.Request.Context().Deadline()
}


// Value 会优先返回元数据中的Key,如果没有才会尝试去Request中的Context中查询。
func (c *Context) Value(key any) any {
    //两种特殊情况
    if key == 0 {
        return c.Request //返回Request结构体
    }
    if key == ContextKey {
        return c //返回Context本身
    }
    if keyAsString, ok := key.(string); ok {
        if val, exists := c.Get(keyAsString); exists {
            return val
        }
    }
    if !c.engine.ContextWithFallback || c.Request == nil || c.Request.Context() == nil {
        return nil
    }
    return c.Request.Context().Value(key)
}

在上下文中传递数据

SetGet,为此上下文存储新的键值对。存储在Context中的Keys数据字段中,如果以前没有使用过,它会延迟初始化。

func (c *Context) Set(key string, value any) {
   c.mu.Lock()
   defer c.mu.Unlock()
   if c.Keys == nil {
      c.Keys = make(map[string]any)
   }

   c.Keys[key] = value
}

func (c *Context) Get(key string) (value any, exists bool) {
   c.mu.RLock()
   defer c.mu.RUnlock()
   value, exists = c.Keys[key]
   return
}

获取Param数据

func (c *Context) Param(key string) string {
   return c.Params.ByName(key)
}

举一个例子

r.GET("/video/:id", func(context *gin.Context) {})

curl --location --request GET http://localhost:8080/video/1

id := c.Param("id") // id = 1

获取Query数据

// TODO

获取header数据

// TODO

获取body数据

// TODO

请求流转

请求是如何从 gin 流入 net/http, 最后又回到 gin

//Run将路由器连接到http.Server,并开始侦听和提供http请求。
//这是http.ListnAndServe(地址,路由器)的快捷方式
//注意:除非发生错误,否则此方法将无限期地阻止调用goroutine。
func (engine *Engine) Run(addr ...string) (err error) {
   defer func() { debugPrintError(err) }()

   if engine.isUnsafeTrustedProxies() {
      debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
         "Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")
   }

   address := resolveAddress(addr)
   debugPrint("Listening and serving HTTP on %s\n", address)
   err = http.ListenAndServe(address, engine.Handler())
   return
}

通过 http.ListenAndServe(),将 gin 自己实现的处理器传入,然后通过 net/http 建立连接

type Handler interface {
   ServeHTTP(ResponseWriter, *Request)
}


func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
   handler := sh.srv.Handle
   ...

   handler.ServeHTTP(rw, req)
}

sh.srv.Handle 是一个接口,其真正的类型是 gin.Engine,根据 interace 的动态转发特性,最终会跳转到 gin.Engine.ServeHTTP 函数中

// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {

   // 从 sync.pool 里面取一块内存
   c := engine.pool.Get().(*Context)
   c.writermem.reset(w)
   c.Request = req
   c.reset()

   // 处理请求
   engine.handleHTTPRequest(c)

   // 请求处理结束,归还内粗
   engine.pool.Put(c)
}