gin源码研究&整理之Context类

以下研究内容源于1.4.0版本源码

Context扮演的角色

每个HTTP请求都会包含一个Context对象,Context应贯穿整个HTTP请求,包含所有上下文信息

Context对象池

为了减少GC,gin使用了对象池管理Context

//gin.go
// ServeHTTP conforms to the http.Handler interface.
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在goroutine中的并发安全

gin没有对Context并发安全的处理,应避免多goroutine同时访问同一个Context。如可能存在goroutine同时访问Context的情况,应用事先用Copy方法进行拷贝,如下:

// Copy returns a copy of the current context that can be safely used outside the request's scope.
// This has to be used when the context has to be passed to a goroutine.
func (c *Context) Copy() *Context {
    var cp = *c
    cp.writermem.ResponseWriter = nil
    cp.Writer = &cp.writermem
    cp.index = abortIndex
    cp.handlers = nil
    cp.Keys = map[string]interface{}{}
    for k, v := range c.Keys {
        cp.Keys[k] = v
    }
    return &cp
}

可以看到拷贝之后,ResponseWriter其实是一个空的对象,所以说,即使拷贝了,也要在主Context中才能返回响应结果。
这样设计是好的,如果在Context中处理了并发安全,会代码降低执行效率不说,使用者滥用goroutine的话,响应流程就处理混乱了。
整理后决定连Copy方法也删了。

Context之Bind

Bind、ShouldBind相关方法用于请求参数绑定,区别是Bind绑定过程中出现error会直接返回HTTP异常码。

关于ShouldBindBodyWith

// ShouldBindBodyWith is similar with ShouldBindWith, but it stores the request
// body into the context, and reuse when it is called again.
//
// NOTE: This method reads the body before binding. So you should use
// ShouldBindWith for better performance if you need to call only once.
func (c *Context) ShouldBindBodyWith(obj interface{}, bb binding.BindingBody) (err error) {
    var body []byte
    if cb, ok := c.Get(BodyBytesKey); ok {
        if cbb, ok := cb.([]byte); ok {
            body = cbb
        }
    }
    if body == nil {
        body, err = ioutil.ReadAll(c.Request.Body)
        if err != nil {
            return err
        }
        c.Set(BodyBytesKey, body)
    }
    return bb.BindBody(body, obj)
}

这个方法没有用到,作用是先把Body备份一份到Context,下次数据绑定直接从Context中取。没有意义,重新解析一次和直接用Bind没有区别,删掉。

Context之Negotiate

设计的初衷是根据客户端请求,返回客户端需要的数据格式,如果不能提供,就返回默认格式

// Negotiate contains all negotiations data.
type Negotiate struct {
    Offered  []string
    HTMLName string
    HTMLData interface{}
    JSONData interface{}
    XMLData  interface{}
    Data     interface{}
}

特殊场景会用到,实际不如直接switch来得快。其实支持多返回结果,实际用的也是单单Data字段,否则就要在内存中生成多份数据,影响效率。比如同时支持JSON和XML,Negotiate里就要同时包含JSONData和XMLData,实际上只包含一个Data就可以了。这里是过度设计,可删除。

Context之响应(以json格式为例)

// JSON serializes the given struct as JSON into the response body.
// It also sets the Content-Type as "application/json".
func (c *Context) JSON(code int, obj interface{}) {
    c.Render(code, render.JSON{Data: obj})
}

// AbortWithStatusJSON calls `Abort()` and then `JSON` internally.
// This method stops the chain, writes the status code and return a JSON body.
// It also sets the Content-Type as "application/json".
func (c *Context) AbortWithStatusJSON(code int, jsonObj interface{}) {
    c.Abort()//停止下一个路由方法的访问,返回当前写入的请求结果。这行代码放在下行代码后结果是一样的
    c.JSON(code, jsonObj)
}

如上,只有调用Abort()之后,HTTP请求才会马上返回响应结果,否则,会执行下一个路由方法。
既然都传入http返回状态码了,常规情况就应该是直接Abort()。而且正常返回流程HTTP状态码就是200。

而且一个有意思的情况是,如果你这样调用

c.JSON(200,...)
c.JSON(200,...)
c.Abort()

会打印一个[重复写入HTTP状态码]的警告:[WARNING] Headers were already written. Wanted to override status code 我们来看警告的源码

func (w *responseWriter) WriteHeader(code int) {
    if code > 0 && w.status != code {
        if w.Written() {
            debugPrint("[WARNING] Headers were already written. Wanted to override status code %d with %d", w.status, code)
        }
        w.status = code
    }
}

然后再看gin自带的logger里做了这种事情

//logger.go 169行
// ErrorLoggerT returns a handlerfunc for a given error type.
func ErrorLoggerT(typ ErrorType) HandlerFunc {
    return func(c *Context) {
        c.Next()
        errors := c.Errors.ByType(typ)
        if len(errors) > 0 {
            c.JSON(-1, errors)
        }
    }
}

惊不惊喜,意不意外?也就是说用到gin自带的logger的时候,还可能给你带来个彩蛋。可能会返回这样的数据给前端:[一串JSON的错误信息]+[正常返回数据]。

c.JSON(-1, errors)//这里因为code<0,不会引发[WARNING] Headers were already written的后台错误
c.JSON(200,gin.H{"code":500,"message":"用户名不能为空"})
c.Abort()
//这里因为连续两次写入JSON数据,前端收到HTTP状态码是200,但是无法识别正常数据。

程序设计应避免这种模棱两可的情况。还有自带logger最好不用吧、想办法清理掉换上自己的日志库。

所以说,考虑通常情况,简化调用流程,改良后代码:

func (c *Context) JSON(obj interface{}) {
    c.Abort()
    c.JSONWithStatus(http.StatusOK, jsonObj)
}

func (c *Context) JSONWithStatus(code int, jsonObj interface{}) {
    c.Render(code, render.JSON{Data: obj})
    c.Abort()
}

相比原来舒服多了。这样就够了吗?还不够。因为除了JSON还有好多种数据格式返回,那样每种数据格式,就要开放两个方法。然后继续研究代码。

如下,发现最后面都会调用到此方法,这个方法还是public的

// Status sets the HTTP response code.(设置HTTP返回状态码)
func (c *Context) Status(code int) {
    c.writermem.WriteHeader(code)
}

那如果不设置的话,默认状态码是多少呢?没错,下面这个defaultStatus就是200

func (w *responseWriter) reset(writer http.ResponseWriter) {
    w.ResponseWriter = writer
    w.size = noWritten
    w.status = defaultStatus
}

那么就好办了,只保留一个方法即可

func (c *Context) JSON(obj interface{}) {
    c.Render(c.writermem.status, render.JSON{Data: obj})
    c.Abort()
}

//调用示例1--常规返回(200)
c.JSON("{}")
//调用示例2--指定状态码返回
c.Status(500)
c.JSON("{}")

总结

1.gin使用对象池高效地管理Context。
2.gin的Context不是并发安全的,应注意避免。
3.Bind、ShouldBind相关方法用于请求参数绑定,区别是Bind绑定过程中出现error会直接返回HTTP异常码。
4.Negotiate为过度设计,可删除。
5.Context的响应方法可以加上Abort和默认HTTP状态码,用得更舒服点。还能避免踩坑。

另附一份修改过的context.go文件的代码代码链接

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

推荐阅读更多精彩内容