拆轮子系列:gin框架

关于WEB框架

由于现在编程的语言变成go了,所以拆轮子系列,拆的轮子也是go方面的了,其实也不要紧,因为处理的思路是和语言无关的。gin是go的轻量级的web框架,轻量级意味着仅仅提供web框架应有的基础功能。我觉得看源码最好就是要有目标,看gin这个web框架,我的目标是:

  1. gin这个web框架是怎么实现web框架应有的基础功能的
  2. 代码上实现上有什么值得学习的地方。

一次请求处理的大体流程

如何找到入口

要知道一次请求处理的大体流程,只要找到web框架的入口即可。先看看gin文档当中最简单的demo。Run方法十分耀眼,点击去可以看到关键的http.ListenAndServe,这意味着Engine这个结构体,实现了ServeHTTP这个接口。入口就是Engine实现的ServeHTTP接口。

//我是最简单的demo
func main() {
    r := gin.Default()
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "pong",
        })
        c.Redirect(http.StatusMovedPermanently, "https://github.com/gin-gonic/gin")

    })
    r.Run() // listen and serve on 0.0.0.0:8080
}

//我是Run方法
func (engine *Engine) Run(addr ...string) (err error) {
    defer func() { debugPrintError(err) }()

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

ServeHTTP

大体流程就如注释那样,那么的简单。这里值得关注的是,Context这个上下文对象是在对象池里面取出来的,而不是每次都生成,提高效率。可以看到,真正的核心处理流程是在handleHTTPRequest方法当中。

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {

    // 从上下文对象池中获取一个上下文对象
    c := engine.pool.Get().(*Context)

    // 初始化上下文对象,因为从对象池取出来的数据,有脏数据,故要初始化。
    c.writermem.reset(w)
    c.Request = req
    c.reset()

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

    //将Context对象扔回对象池了
    engine.pool.Put(c)
}

handleHTTPRequest

下面的代码省略了很多和核心逻辑无关的代码,核心逻辑很简单:更具请求方法和请求的URI找到处理函数们,然后调用。为什么是处理函数们,而不是我们写的处理函数?因为这里包括了中间层的处理函数。

func (engine *Engine) handleHTTPRequest(context *Context) {
    httpMethod := context.Request.Method
    var path string
    var unescape bool

    // 省略......
    // tree是个数组,里面保存着对应的请求方式的,URI与处理函数的树。
    // 之所以用数组是因为,在个数少的时候,数组查询比字典要快
    t := engine.trees
    for i, tl := 0, len(t); i < tl; i++ {
        
        if t[i].method == httpMethod {
            root := t[i].root
            
            // 找到路由对应的处理函数们
            handlers, params, tsr := root.getValue(path, context.Params, unescape)
            
            // 调用处理函数们
            if handlers != nil {
                context.handlers = handlers
                context.Params = params
                context.Next()
                context.writermem.WriteHeaderNow()
                return
            }
            
            // 省略......
            break
        }
    }

    // 省略......
}

值得欣赏学习的地方

路由处理

关键需求

先抛开gin框架不说,路由处理的关键需求有哪些?个人认为有以下两点

  • 高效的URI对应的处理函数的查找
  • 灵活的路由组合

gin的处理

核心思路

  • 每一个路由对应的都有一个独立的处理函数数组
  • 中间件与处理函数是一致的
  • 利用树提供高效的URI对应的处理函数数组的查找

有趣的地方

RouterGroup对路由的处理

灵活的路由组合是通过将每一个URI都应用着一个独立的处理函数数组来实现的。对于路由组合的操作抽象出了RouterGroup结构体来应对。它的主要作用是:

  • 将路由与相关的处理函数关联起来
  • 提供了路由组的功能,这个是由于关联前缀的方式实现的
  • 提供了中间件自由组合的功能:1. 总的中间件 2. 路由组的中间件 3.处理函数的中间件

路由组和处理函数都可以添加中间件这比DJango那种只有总的中间件要灵活得多。

中间件的处理

中间件在请求的时候需要处理,在返回时也可能需要做处理。如下图(图是django的)。

中间件的处理.png

问题来了在gin中间件就是一个处理函数,怎么实现返回时的处理呢。仔细观察,上面图的调用,就是后进先出,是的每错答案就是:利用函数调用栈后进先出的特点,巧妙的完成中间件在自定义处理函数完成的后处理的操作。django它的处理方式是定义个类,请求处理前的处理的定义一个方法,请求处理后的处理定义一个方法。gin的方式更灵活,但django的方式更加清晰。

//调用处理函数数组
func (c *Context) Next() {
    c.index++
    s := int8(len(c.handlers))
    for ; c.index < s; c.index++ {
        c.handlers[c.index](c)
    }
}

// 中间件例子
func Logger() gin.HandlerFunc {
    return func(c *gin.Context) {
        t := time.Now()

        // before request
        c.Set("example", "12345")

        c.Next()

        // 返回后的处理
        latency := time.Since(t)
        log.Print("latency: ", latency)

        status := c.Writer.Status()
        log.Println("status: ", status)
    }
}

func main() {
    r := gin.New()
    r.Use(Logger())

    r.GET("/test", func(c *gin.Context) {
        example := c.MustGet("example").(string)

        // it would print: "12345"
        log.Println("example", example)
    })

    // Listen and serve on 0.0.0.0:8080
    r.Run(":8081")
}

请求内容的处理与返回内容的处理

需求

  • 获取路径当中的参数
  • 获取请求参数
  • 获取请求内容
  • 将处理好的结果返回

Gin框架的实现思路

自己包装一层除了能提供体验一致的处理方法之外,如果对官方实现的不爽,可以替换掉,甚至可以加一层缓存处理(其实没必要,因为正常的使用,仅仅只会处理一次就够了)。

  • 如果官方的http库能提供的,则在官方的http库只上包装一层,提供体验一致的接口。
  • 官方http库不能提供的,则自己实现

关键结构体

type Context struct {
    writermem responseWriter
    Request   *http.Request

    // 传递接口,使用各个处理函数,更加灵活,降低耦合
    Writer    ResponseWriter
    
    Params   Params             // 路径当中的参数
    handlers HandlersChain      // 处理函数数组
    index    int8               // 目前在运行着第几个处理函数

    engine   *Engine
    Keys     map[string]interface{}  // 各个中间件添加的key value
    Errors   errorMsgs
    Accepted []string
}

值得学习的点

在数量少的情况下用数组查找值,比用字典查找值要快

在上面对Context结构体的注释当中,可以知道Params其实是个数组。本质上可以说是key值的对应,为啥不用字典呢,而是用数组呢? 实际的场景,获取路径参数的参数个数不会很多,如果用字典性能反而不如数组高。因为字典要找到对应的值,大体的流程:对key进行hash —> 通过某算法找到对应偏移的位置(有好几种算法,有兴趣的可以去查查看) —> 取值。一套流程下来,数组在量少的情况下,已经遍历完了。

router.GET("user/:name/*action", func(c *gin.Context) {
        name := c.Param("name")
        action := c.Param("action")
        message := name + " is222 " + action
        c.String(http.StatusOK, message)
    })
   
func (ps Params) Get(name string) (string, bool) {
    for _, entry := range ps {
        if entry.Key == name {
            return entry.Value, true
        }
    }
    return "", false
}

通过接口处理有所同,有所不同的场景

获取请求内容

对于获取请求内容这个需求面临着的场景。对于go这种静态语言来说,如果要对请求内容进行处理,就需要对内容进行反序列化到某个结构体当中,然而请求内容的形式多种多样,例如:JSON,XML,ProtoBuf等等。因此这里可以总结出下面的非功能性需求。

  • 不同的内容需要不同的反序列化机制
  • 允许用户自己实现反序列化机制

共同点都是对内容做处理,不同点是对内容的处理方式不一样,很容易让人想到多态这概念,异种求同。多态的核心就是接口,这时候需要抽象出一个接口。

type Binding interface {
    Name() string
    Bind(*http.Request, interface{}) error
}

将处理好的内容返回

请求内容多种多样,返回的内容也是一样的。例如:返回JSON,返回XML,返回HTML,返回302等等。这里可以总结出以下非功能性需求。

  • 不同类型的返回内容需要不同的序列化机制
  • 允许用户实现自己的序列化机制

和上面的一致的,因此这里也抽象出一个接口。

type Render interface {
    Render(http.ResponseWriter) error
    WriteContentType(w http.ResponseWriter)
}

接口定义好之后需要思考如何使用接口

思考如何优雅的使用这些接口

对于获取请求内容,在模型绑定当中,有以下的场景

  • 绑定失败是用户自己处理还是框架统一进行处理
  • 用户需是否需要关心请求的内容选择不同的绑定器

在gin框架的对于这些场景给出的答案是:提供不同的方法,满足以上的需求。这里的关键点还是在于使用场景是怎样的。

// 自动更加请求头选择不同的绑定器对象进行处理
func (c *Context) Bind(obj interface{}) error {
    b := binding.Default(c.Request.Method, c.ContentType())
    return c.MustBindWith(obj, b)
}

// 绑定失败后,框架会进行统一的处理
func (c *Context) MustBindWith(obj interface{}, b binding.Binding) (err error) {
    if err = c.ShouldBindWith(obj, b); err != nil {
        c.AbortWithError(400, err).SetType(ErrorTypeBind)
    }

    return
}

// 用户可以自行选择绑定器,自行对出错处理。自行选择绑定器,这也意味着用户可以自己实现绑定器。
// 例如:嫌弃默认的json处理是用官方的json处理包,嫌弃它慢,可以自己实现Binding接口
func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error {
    return b.Bind(c.Request, obj)
}

对于实现的结构体构造不一致的处理

将处理好的内容返回,实现的类构造参数都是不一致的。例如:对于文本的处理和对于json的处理。面对这种场景祭出的武器是:封装多一层,用于构造出相对于的处理对象。

//对于String的处理
type String struct {
    Format string
    Data   []interface{}
}

//对于String处理封装多的一层 
func (c *Context) String(code int, format string, values ...interface{}) {
    c.Render(code, render.String{Format: format, Data: values})
}

//对于json的处理
JSON struct {
    Data interface{}
}

//对于json的处理封装多的一层
func (c *Context) JSON(code int, obj interface{}) {
    c.Render(code, render.JSON{Data: obj})
}

//核心的一致的处理
func (c *Context) Render(code int, r render.Render) {
    c.Status(code)

    if !bodyAllowedForStatus(code) {
        r.WriteContentType(c.Writer)
        c.Writer.WriteHeaderNow()
        return
    }

    if err := r.Render(c.Writer); err != nil {
        panic(err)
    }
}

总结

这个看代码的过程是在有目标之后,按照官方文档的例子,一步一步的看的。然后再慢慢欣赏,这框架对于一些web框架常见的场景,它是怎么处理。这框架的代码量很少,而且写得十分的优雅,非常值得一看。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容