GO源码走读--gin框架概览

2024-07-17  本文已影响0人  温岭夹糕

目录

链接地址

1.框架概览

快问快答
Q:首先,什么是网络框架?
A: 对于GO来说,网络框架就是介于开发者网络库net/http之间抽象出来的一层或一个工具,来达成帮助我们快速开发应用的目的,而net/http又搭建了用户态到内核态(指网络调用方面)的桥梁

image.png
那么gin框架是如何抽象出它和net/http的关系呢?

1.1使用实例

本节源码

func main() {
    serve := gin.Default()

    serve.Use(func(ctx *gin.Context) {
        fmt.Println("hi")
    })

    serve.GET("home", func(ctx *gin.Context) {
        ctx.JSON(http.StatusOK, gin.H{"message": "hi"})
    })

    v1 := serve.Group("/v1")
    {
        v1.GET("/login", func(ctx *gin.Context) {
            ctx.JSON(http.StatusOK, gin.H{"message": "hi"})
        })
    }
    // serve.Run(":8088")
    http.ListenAndServe(":8088", serve)
}

gin.Default返回Engine的指针类型
gin既可以通过自身的Engine.Run方法启动,也可以将Engine注入到ListenAndServe中启动服务器,说明了gin本身的关键数据结构Engine实现了http.Handler接口

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}
即gin框架是通过http.Handler来搭建其和net/http库的关系,再大胆点思考,是不是意味着我们实现了该接口,也能手撸出一个web框架出来? image.png

2.核心数据结构Engine

func Default(opts ...OptionFunc) *Engine

核心成员如下

type Engine struct {
   // 路由组
    RouterGroup
    // ...
    // context 对象池
    pool             sync.Pool
    // 方法路由树
    trees            methodTrees
    // ...
}

2.1RouteGroup

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

var _ IRouter = (*RouterGroup)(nil)
RouteGroup作为内嵌字段被嵌入Engine,是路由组的概念,其中的配置将被从属于该路由组的所有路由所复用,同时也是实现了IRouter接口 image.png

从该接口规定的方法来看,实际上它就是真正的注册路由结构体

Engine.Get()

实际上就是

RouterGoup.Get()
type HandlersChain []HandlerFunc

2.2 Engine创建流程

即Default函数
方法调用:gin.Default -> gin.New

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

func New() *Engine {
    // ...
    // 创建 gin Engine 实例
    engine := &Engine{
        // 路由组实例
        RouterGroup: RouterGroup{
            Handlers: nil,
            basePath: "/",
            root:     true,
        },
        // ...
        // 9 棵路由压缩前缀树,对应 9 种 http 方法
        trees:                  make(methodTrees, 0, 9),
        // ...
    }
    engine.RouterGroup.engine = engine     
    // gin.Context 对象池   
    engine.pool.New = func() any {
        return engine.allocateContext(engine.maxParams)
    }
    return engine
}

2.3注册中间件

即Use方法,不断向RouterGroup尾部加入HandlerFunc
方法调用: Engine.Use ->RouterGroup.Use

func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
    group.Handlers = append(group.Handlers, middleware...)
       //根据树是否为nil返回engine或自身
    return group.returnObj()
}

2.4路由注册

以Get为例
方法调用: engine.Get -> RouterGroup.Get -> RouterGroup.handle

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

关于gin中的压缩前缀树看这里

2.5服务启动

底层也是调用listenandserver

func (engine *Engine) Run(addr ...string) (err error) {
    // ...
    err = http.ListenAndServe(address, engine.Handler())
    return
}

2.6处理请求

即Engine.ServerHttp函数

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

因为请求的创建和销毁是很频繁的,这也意味着context的创建和销毁也是很频繁的,因此从池中取,用完放回池中,懒加载模式重置context(这里的context实际上是对http.ResponseWriter 和http.Request的封装)

func (engine *Engine) handleHTTPRequest(c *Context) {
    httpMethod := c.Request.Method
    rPath := c.Request.URL.Path
    
    // ...
    t := engine.trees
        if value.handlers != nil {
            c.handlers = value.handlers
            c.fullPath = value.fullPath
            c.Next()
            c.writermem.WriteHeaderNow()
            return
        }

通过调用context.Next方法执行之前注册的函数

3 核心数据结构Context

type Context struct {
    // ...
    // http 请求参数
    Request   *http.Request
    // http 响应 writer
    Writer    ResponseWriter
    // ...
    // 处理函数链
    handlers HandlersChain
    // 当前处于处理函数链的索引
    index    int8
    engine       *Engine
    // ...
    // 读写锁,保证并发安全
    mu sync.RWMutex
    // key value 对存储 map
    Keys map[string]any
    // ..
}

这里重点分析它是如何对handlers的遍历,即Next方法
进入next前,index=-1

func (c *Context) Next() {
    c.index++
    for c.index < int8(len(c.handlers)) {
        c.handlers[c.index](c)
        c.index++
    }
}

在我们的印象里,中间件通常都是洋葱模式的,通过index和for循环依次调用handlers,那这种遍历调用是不是就意味着它不是洋葱模式了?
我们写一个代码看一下

    serve.Use(func(ctx *gin.Context) {
// handle1
        fmt.Println(1)
        ctx.Next()
        fmt.Println(4)
    }, func(ctx *gin.Context) {
// handle2
        fmt.Println(2)
        ctx.Next()
        fmt.Println(3)
    })

1
2
3
4

发现它还是洋葱模式,我们一步步分析来看一下它是如何用index来模拟压栈流程的(实际上就是利用全局变量写递归,熟悉递归的后面就跳过),我们分别定义上面两个为handl1和2,主业务为handlFunc(先不管logger和recover中间件)

2.1利用Abort停止handle传递

// 127>>1 = 63
const abortIndex int8 = math.MaxInt8 >> 1


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

func (c *Context) IsAborted() bool {
    return c.index >= abortIndex
}

其实现原理是将 Context.index 设置为一个过载值 63,导致 Next 流程直接终止. 这是因为 handlers 链的长度必须小于 63,否则在注册时就会直接 panic. 因此在 Context.Next 方法中,一旦 index 被设为 63,则必然大于整条 handlers 链的长度,for 循环便会提前终止.

func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
    finalSize := len(group.Handlers) + len(handlers)
    // 断言 handlers 链长度必须小于 63
    assert1(finalSize < int(abortIndex), "too many handlers")
    // ...
}

此外,用户还可以通过 Context.IsAbort 方法检测当前 handlerChain 是出于正常调用,还是已经被熔断.

4.总结与思考

image.png

gin通过http.Hanlder接口搭建了自己和http包的桥梁,其框架核心就是Server(Engine)、路由树(RouterGroup)和上下文(Context)

那也就意味着我们设计好这三方面也能实现一个最简单的路由框架

上一篇 下一篇

猜你喜欢

热点阅读