gin源碼研究&整理之Context類

以下研究?jī)?nèi)容源于1.4.0版本源碼

Context扮演的角色

每個(gè)HTTP請(qǐng)求都會(huì)包含一個(gè)Context對(duì)象迫皱,Context應(yīng)貫穿整個(gè)HTTP請(qǐng)求,包含所有上下文信息

Context對(duì)象池

為了減少GC和敬,gin使用了對(duì)象池管理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中的并發(fā)安全

gin沒有對(duì)Context并發(fā)安全的處理戏阅,應(yīng)避免多goroutine同時(shí)訪問同一個(gè)Context。如可能存在goroutine同時(shí)訪問Context的情況舱痘,應(yīng)用事先用Copy方法進(jìn)行拷貝,如下:

// 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其實(shí)是一個(gè)空的對(duì)象铝耻,所以說蹬刷,即使拷貝了,也要在主Context中才能返回響應(yīng)結(jié)果泡态。
這樣設(shè)計(jì)是好的迂卢,如果在Context中處理了并發(fā)安全而克,會(huì)代碼降低執(zhí)行效率不說,使用者濫用goroutine的話腾降,響應(yīng)流程就處理混亂了碎绎。
整理后決定連Copy方法也刪了。

Context之Bind

Bind奸晴、ShouldBind相關(guān)方法用于請(qǐng)求參數(shù)綁定幕随,區(qū)別是Bind綁定過程中出現(xiàn)error會(huì)直接返回HTTP異常碼赘淮。

關(guān)于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)
}

這個(gè)方法沒有用到,作用是先把Body備份一份到Context走诞,下次數(shù)據(jù)綁定直接從Context中取蛤高。沒有意義,重新解析一次和直接用Bind沒有區(qū)別塞绿,刪掉异吻。

Context之Negotiate

設(shè)計(jì)的初衷是根據(jù)客戶端請(qǐng)求,返回客戶端需要的數(shù)據(jù)格式棋返,如果不能提供雷猪,就返回默認(rèn)格式

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

特殊場(chǎng)景會(huì)用到,實(shí)際不如直接switch來得快射沟。其實(shí)支持多返回結(jié)果月帝,實(shí)際用的也是單單Data字段,否則就要在內(nèi)存中生成多份數(shù)據(jù)簿姨,影響效率簸搞。比如同時(shí)支持JSON和XML趁俊,Negotiate里就要同時(shí)包含JSONData和XMLData,實(shí)際上只包含一個(gè)Data就可以了暇务。這里是過度設(shè)計(jì)怔软,可刪除。

Context之響應(yīng)(以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()//停止下一個(gè)路由方法的訪問挡逼,返回當(dāng)前寫入的請(qǐng)求結(jié)果括改。這行代碼放在下行代碼后結(jié)果是一樣的
    c.JSON(code, jsonObj)
}

如上,只有調(diào)用Abort()之后家坎,HTTP請(qǐng)求才會(huì)馬上返回響應(yīng)結(jié)果嘱能,否則吝梅,會(huì)執(zhí)行下一個(gè)路由方法。
既然都傳入http返回狀態(tài)碼了惹骂,常規(guī)情況就應(yīng)該是直接Abort()苏携。而且正常返回流程HTTP狀態(tài)碼就是200。

而且一個(gè)有意思的情況是兜叨,如果你這樣調(diào)用

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

會(huì)打印一個(gè)[重復(fù)寫入HTTP狀態(tài)碼]的警告:[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的時(shí)候,還可能給你帶來個(gè)彩蛋矛物∶K溃可能會(huì)返回這樣的數(shù)據(jù)給前端:[一串JSON的錯(cuò)誤信息]+[正常返回?cái)?shù)據(jù)]。

c.JSON(-1, errors)//這里因?yàn)閏ode<0履羞,不會(huì)引發(fā)[WARNING] Headers were already written的后臺(tái)錯(cuò)誤
c.JSON(200,gin.H{"code":500,"message":"用戶名不能為空"})
c.Abort()
//這里因?yàn)檫B續(xù)兩次寫入JSON數(shù)據(jù)峦萎,前端收到HTTP狀態(tài)碼是200,但是無法識(shí)別正常數(shù)據(jù)忆首。

程序設(shè)計(jì)應(yīng)避免這種模棱兩可的情況爱榔。還有自帶logger最好不用吧、想辦法清理掉換上自己的日志庫糙及。

所以說详幽,考慮通常情況,簡(jiǎn)化調(diào)用流程浸锨,改良后代碼:

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

相比原來舒服多了唇聘。這樣就夠了嗎?還不夠柱搜。因?yàn)槌薐SON還有好多種數(shù)據(jù)格式返回迟郎,那樣每種數(shù)據(jù)格式,就要開放兩個(gè)方法聪蘸。然后繼續(xù)研究代碼宪肖。

如下,發(fā)現(xiàn)最后面都會(huì)調(diào)用到此方法健爬,這個(gè)方法還是public的

// Status sets the HTTP response code.(設(shè)置HTTP返回狀態(tài)碼)
func (c *Context) Status(code int) {
    c.writermem.WriteHeader(code)
}

那如果不設(shè)置的話控乾,默認(rèn)狀態(tài)碼是多少呢?沒錯(cuò)浑劳,下面這個(gè)defaultStatus就是200

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

那么就好辦了阱持,只保留一個(gè)方法即可

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

//調(diào)用示例1--常規(guī)返回(200)
c.JSON("{}")
//調(diào)用示例2--指定狀態(tài)碼返回
c.Status(500)
c.JSON("{}")

總結(jié)

1.gin使用對(duì)象池高效地管理Context。
2.gin的Context不是并發(fā)安全的魔熏,應(yīng)注意避免衷咽。
3.Bind鸽扁、ShouldBind相關(guān)方法用于請(qǐng)求參數(shù)綁定,區(qū)別是Bind綁定過程中出現(xiàn)error會(huì)直接返回HTTP異常碼镶骗。
4.Negotiate為過度設(shè)計(jì)桶现,可刪除。
5.Context的響應(yīng)方法可以加上Abort和默認(rèn)HTTP狀態(tài)碼鼎姊,用得更舒服點(diǎn)骡和。還能避免踩坑。

另附一份修改過的context.go文件的代碼代碼鏈接

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末相寇,一起剝皮案震驚了整個(gè)濱河市慰于,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌唤衫,老刑警劉巖婆赠,帶你破解...
    沈念sama閱讀 211,290評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異佳励,居然都是意外死亡休里,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門赃承,熙熙樓的掌柜王于貴愁眉苦臉地迎上來妙黍,“玉大人,你說我怎么就攤上這事瞧剖∈眉蓿” “怎么了?”我有些...
    開封第一講書人閱讀 156,872評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵抓于,是天一觀的道長(zhǎng)噩凹。 經(jīng)常有香客問我,道長(zhǎng)毡咏,這世上最難降的妖魔是什么驮宴? 我笑而不...
    開封第一講書人閱讀 56,415評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮呕缭,結(jié)果婚禮上堵泽,老公的妹妹穿的比我還像新娘。我一直安慰自己恢总,他們只是感情好迎罗,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,453評(píng)論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著片仿,像睡著了一般纹安。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,784評(píng)論 1 290
  • 那天厢岂,我揣著相機(jī)與錄音光督,去河邊找鬼。 笑死塔粒,一個(gè)胖子當(dāng)著我的面吹牛结借,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播卒茬,決...
    沈念sama閱讀 38,927評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼船老,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了圃酵?” 一聲冷哼從身側(cè)響起柳畔,我...
    開封第一講書人閱讀 37,691評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎郭赐,沒想到半個(gè)月后荸镊,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,137評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡堪置,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,472評(píng)論 2 326
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了张惹。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片舀锨。...
    茶點(diǎn)故事閱讀 38,622評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖宛逗,靈堂內(nèi)的尸體忽然破棺而出坎匿,到底是詐尸還是另有隱情,我是刑警寧澤雷激,帶...
    沈念sama閱讀 34,289評(píng)論 4 329
  • 正文 年R本政府宣布替蔬,位于F島的核電站,受9級(jí)特大地震影響屎暇,放射性物質(zhì)發(fā)生泄漏承桥。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,887評(píng)論 3 312
  • 文/蒙蒙 一根悼、第九天 我趴在偏房一處隱蔽的房頂上張望凶异。 院中可真熱鬧,春花似錦挤巡、人聲如沸剩彬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽喉恋。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間轻黑,已是汗流浹背糊肤。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留苔悦,地道東北人轩褐。 一個(gè)月前我還...
    沈念sama閱讀 46,316評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像玖详,于是被迫代替她去往敵國(guó)和親把介。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,490評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容

  • 轉(zhuǎn)發(fā)自:http://shanshanpt.github.io/2016/05/03/go-gin.html gi...
    dncmn閱讀 6,030評(píng)論 0 1
  • 介紹 Gin 是一個(gè)用 Go (Golang) 編寫的 web 框架蟋座。 它是一個(gè)類似于 martini 但擁有更好...
    零一間閱讀 21,048評(píng)論 0 24
  • 所謂框架 框架一直是敏捷開發(fā)中的利器拗踢,能讓開發(fā)者很快的上手并做出應(yīng)用,甚至有的時(shí)候向臀,脫離了框架巢墅,一些開發(fā)者都不會(huì)寫...
    人世間閱讀 216,155評(píng)論 11 242
  • 開發(fā)go程序的時(shí)候,時(shí)常需要使用goroutine并發(fā)處理任務(wù)券膀,有時(shí)候這些goroutine是相互獨(dú)立的君纫,而有的時(shí)...
    駐馬聽雪閱讀 2,429評(píng)論 0 21
  • 本來自己打算繼續(xù)學(xué)下beanFactory源碼的,但是放假了自己也沒什么精神芹彬,看源碼又要求注意力很集中蓄髓,所以想著看...
    非典型_程序員閱讀 59,753評(píng)論 3 20