拆輪子系列:gin框架

關(guān)于WEB框架

由于現(xiàn)在編程的語(yǔ)言變成go了娶靡,所以拆輪子系列牧牢,拆的輪子也是go方面的了,其實(shí)也不要緊,因?yàn)樘幚淼乃悸肥呛驼Z(yǔ)言無(wú)關(guān)的塔鳍。gin是go的輕量級(jí)的web框架伯铣,輕量級(jí)意味著僅僅提供web框架應(yīng)有的基礎(chǔ)功能。我覺得看源碼最好就是要有目標(biāo)轮纫,看gin這個(gè)web框架腔寡,我的目標(biāo)是:

  1. gin這個(gè)web框架是怎么實(shí)現(xiàn)web框架應(yīng)有的基礎(chǔ)功能的
  2. 代碼上實(shí)現(xiàn)上有什么值得學(xué)習(xí)的地方。

一次請(qǐng)求處理的大體流程

如何找到入口

要知道一次請(qǐng)求處理的大體流程蜡感,只要找到web框架的入口即可蹬蚁。先看看gin文檔當(dāng)中最簡(jiǎn)單的demo。Run方法十分耀眼郑兴,點(diǎn)擊去可以看到關(guān)鍵的http.ListenAndServe犀斋,這意味著Engine這個(gè)結(jié)構(gòu)體,實(shí)現(xiàn)了ServeHTTP這個(gè)接口情连。入口就是Engine實(shí)現(xiàn)的ServeHTTP接口叽粹。

//我是最簡(jiǎn)單的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

大體流程就如注釋那樣,那么的簡(jiǎn)單却舀。這里值得關(guān)注的是虫几,Context這個(gè)上下文對(duì)象是在對(duì)象池里面取出來的,而不是每次都生成挽拔,提高效率辆脸。可以看到螃诅,真正的核心處理流程是在handleHTTPRequest方法當(dāng)中啡氢。

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

    // 從上下文對(duì)象池中獲取一個(gè)上下文對(duì)象
    c := engine.pool.Get().(*Context)

    // 初始化上下文對(duì)象,因?yàn)閺膶?duì)象池取出來的數(shù)據(jù)术裸,有臟數(shù)據(jù)倘是,故要初始化。
    c.writermem.reset(w)
    c.Request = req
    c.reset()

    //處理web請(qǐng)求
    engine.handleHTTPRequest(c)

    //將Context對(duì)象扔回對(duì)象池了
    engine.pool.Put(c)
}

handleHTTPRequest

下面的代碼省略了很多和核心邏輯無(wú)關(guān)的代碼袭艺,核心邏輯很簡(jiǎn)單:更具請(qǐng)求方法和請(qǐng)求的URI找到處理函數(shù)們搀崭,然后調(diào)用。為什么是處理函數(shù)們猾编,而不是我們寫的處理函數(shù)瘤睹?因?yàn)檫@里包括了中間層的處理函數(shù)。

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

    // 省略......
    // tree是個(gè)數(shù)組答倡,里面保存著對(duì)應(yīng)的請(qǐng)求方式的默蚌,URI與處理函數(shù)的樹。
    // 之所以用數(shù)組是因?yàn)槲郏趥€(gè)數(shù)少的時(shí)候,數(shù)組查詢比字典要快
    t := engine.trees
    for i, tl := 0, len(t); i < tl; i++ {
        
        if t[i].method == httpMethod {
            root := t[i].root
            
            // 找到路由對(duì)應(yīng)的處理函數(shù)們
            handlers, params, tsr := root.getValue(path, context.Params, unescape)
            
            // 調(diào)用處理函數(shù)們
            if handlers != nil {
                context.handlers = handlers
                context.Params = params
                context.Next()
                context.writermem.WriteHeaderNow()
                return
            }
            
            // 省略......
            break
        }
    }

    // 省略......
}

值得欣賞學(xué)習(xí)的地方

路由處理

關(guān)鍵需求

先拋開gin框架不說,路由處理的關(guān)鍵需求有哪些设江?個(gè)人認(rèn)為有以下兩點(diǎn)

  • 高效的URI對(duì)應(yīng)的處理函數(shù)的查找
  • 靈活的路由組合

gin的處理

核心思路

  • 每一個(gè)路由對(duì)應(yīng)的都有一個(gè)獨(dú)立的處理函數(shù)數(shù)組
  • 中間件與處理函數(shù)是一致的
  • 利用樹提供高效的URI對(duì)應(yīng)的處理函數(shù)數(shù)組的查找

有趣的地方

RouterGroup對(duì)路由的處理

靈活的路由組合是通過將每一個(gè)URI都應(yīng)用著一個(gè)獨(dú)立的處理函數(shù)數(shù)組來實(shí)現(xiàn)的锦茁。對(duì)于路由組合的操作抽象出了RouterGroup結(jié)構(gòu)體來應(yīng)對(duì)。它的主要作用是:

  • 將路由與相關(guān)的處理函數(shù)關(guān)聯(lián)起來
  • 提供了路由組的功能叉存,這個(gè)是由于關(guān)聯(lián)前綴的方式實(shí)現(xiàn)的
  • 提供了中間件自由組合的功能:1. 總的中間件 2. 路由組的中間件 3.處理函數(shù)的中間件

路由組和處理函數(shù)都可以添加中間件這比DJango那種只有總的中間件要靈活得多码俩。

中間件的處理

中間件在請(qǐng)求的時(shí)候需要處理,在返回時(shí)也可能需要做處理歼捏。如下圖(圖是django的)稿存。

中間件的處理.png

問題來了在gin中間件就是一個(gè)處理函數(shù),怎么實(shí)現(xiàn)返回時(shí)的處理呢瞳秽。仔細(xì)觀察瓣履,上面圖的調(diào)用,就是后進(jìn)先出练俐,是的每錯(cuò)答案就是:利用函數(shù)調(diào)用棧后進(jìn)先出的特點(diǎn)袖迎,巧妙的完成中間件在自定義處理函數(shù)完成的后處理的操作。django它的處理方式是定義個(gè)類腺晾,請(qǐng)求處理前的處理的定義一個(gè)方法燕锥,請(qǐng)求處理后的處理定義一個(gè)方法。gin的方式更靈活悯蝉,但django的方式更加清晰归形。

//調(diào)用處理函數(shù)數(shù)組
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")
}

請(qǐng)求內(nèi)容的處理與返回內(nèi)容的處理

需求

  • 獲取路徑當(dāng)中的參數(shù)
  • 獲取請(qǐng)求參數(shù)
  • 獲取請(qǐng)求內(nèi)容
  • 將處理好的結(jié)果返回

Gin框架的實(shí)現(xiàn)思路

自己包裝一層除了能提供體驗(yàn)一致的處理方法之外,如果對(duì)官方實(shí)現(xiàn)的不爽鼻由,可以替換掉暇榴,甚至可以加一層緩存處理(其實(shí)沒必要,因?yàn)檎5氖褂梦嗣遥瑑H僅只會(huì)處理一次就夠了)跺撼。

  • 如果官方的http庫(kù)能提供的,則在官方的http庫(kù)只上包裝一層讨彼,提供體驗(yàn)一致的接口歉井。
  • 官方http庫(kù)不能提供的,則自己實(shí)現(xiàn)

關(guān)鍵結(jié)構(gòu)體

type Context struct {
    writermem responseWriter
    Request   *http.Request

    // 傳遞接口哈误,使用各個(gè)處理函數(shù)哩至,更加靈活,降低耦合
    Writer    ResponseWriter
    
    Params   Params             // 路徑當(dāng)中的參數(shù)
    handlers HandlersChain      // 處理函數(shù)數(shù)組
    index    int8               // 目前在運(yùn)行著第幾個(gè)處理函數(shù)

    engine   *Engine
    Keys     map[string]interface{}  // 各個(gè)中間件添加的key value
    Errors   errorMsgs
    Accepted []string
}

值得學(xué)習(xí)的點(diǎn)

在數(shù)量少的情況下用數(shù)組查找值蜜自,比用字典查找值要快

在上面對(duì)Context結(jié)構(gòu)體的注釋當(dāng)中菩貌,可以知道Params其實(shí)是個(gè)數(shù)組。本質(zhì)上可以說是key值的對(duì)應(yīng)重荠,為啥不用字典呢箭阶,而是用數(shù)組呢? 實(shí)際的場(chǎng)景,獲取路徑參數(shù)的參數(shù)個(gè)數(shù)不會(huì)很多仇参,如果用字典性能反而不如數(shù)組高嘹叫。因?yàn)樽值湟业綄?duì)應(yīng)的值,大體的流程:對(duì)key進(jìn)行hash —> 通過某算法找到對(duì)應(yīng)偏移的位置(有好幾種算法诈乒,有興趣的可以去查查看) —> 取值罩扇。一套流程下來,數(shù)組在量少的情況下怕磨,已經(jīng)遍歷完了喂饥。

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
}

通過接口處理有所同,有所不同的場(chǎng)景

獲取請(qǐng)求內(nèi)容

對(duì)于獲取請(qǐng)求內(nèi)容這個(gè)需求面臨著的場(chǎng)景肠鲫。對(duì)于go這種靜態(tài)語(yǔ)言來說员帮,如果要對(duì)請(qǐng)求內(nèi)容進(jìn)行處理,就需要對(duì)內(nèi)容進(jìn)行反序列化到某個(gè)結(jié)構(gòu)體當(dāng)中滩届,然而請(qǐng)求內(nèi)容的形式多種多樣集侯,例如:JSON,XML帜消,ProtoBuf等等棠枉。因此這里可以總結(jié)出下面的非功能性需求。

  • 不同的內(nèi)容需要不同的反序列化機(jī)制
  • 允許用戶自己實(shí)現(xiàn)反序列化機(jī)制

共同點(diǎn)都是對(duì)內(nèi)容做處理泡挺,不同點(diǎn)是對(duì)內(nèi)容的處理方式不一樣辈讶,很容易讓人想到多態(tài)這概念,異種求同娄猫。多態(tài)的核心就是接口贱除,這時(shí)候需要抽象出一個(gè)接口。

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

將處理好的內(nèi)容返回

請(qǐng)求內(nèi)容多種多樣媳溺,返回的內(nèi)容也是一樣的月幌。例如:返回JSON,返回XML悬蔽,返回HTML扯躺,返回302等等。這里可以總結(jié)出以下非功能性需求蝎困。

  • 不同類型的返回內(nèi)容需要不同的序列化機(jī)制
  • 允許用戶實(shí)現(xiàn)自己的序列化機(jī)制

和上面的一致的录语,因此這里也抽象出一個(gè)接口。

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

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

思考如何優(yōu)雅的使用這些接口

對(duì)于獲取請(qǐng)求內(nèi)容禾乘,在模型綁定當(dāng)中澎埠,有以下的場(chǎng)景

  • 綁定失敗是用戶自己處理還是框架統(tǒng)一進(jìn)行處理
  • 用戶需是否需要關(guān)心請(qǐng)求的內(nèi)容選擇不同的綁定器

在gin框架的對(duì)于這些場(chǎng)景給出的答案是:提供不同的方法,滿足以上的需求始藕。這里的關(guān)鍵點(diǎn)還是在于使用場(chǎng)景是怎樣的蒲稳。

// 自動(dòng)更加請(qǐng)求頭選擇不同的綁定器對(duì)象進(jìn)行處理
func (c *Context) Bind(obj interface{}) error {
    b := binding.Default(c.Request.Method, c.ContentType())
    return c.MustBindWith(obj, b)
}

// 綁定失敗后氮趋,框架會(huì)進(jìn)行統(tǒng)一的處理
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
}

// 用戶可以自行選擇綁定器,自行對(duì)出錯(cuò)處理弟塞。自行選擇綁定器凭峡,這也意味著用戶可以自己實(shí)現(xiàn)綁定器。
// 例如:嫌棄默認(rèn)的json處理是用官方的json處理包决记,嫌棄它慢,可以自己實(shí)現(xiàn)Binding接口
func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error {
    return b.Bind(c.Request, obj)
}

對(duì)于實(shí)現(xiàn)的結(jié)構(gòu)體構(gòu)造不一致的處理

將處理好的內(nèi)容返回倍踪,實(shí)現(xiàn)的類構(gòu)造參數(shù)都是不一致的系宫。例如:對(duì)于文本的處理和對(duì)于json的處理。面對(duì)這種場(chǎng)景祭出的武器是:封裝多一層建车,用于構(gòu)造出相對(duì)于的處理對(duì)象扩借。

//對(duì)于String的處理
type String struct {
    Format string
    Data   []interface{}
}

//對(duì)于String處理封裝多的一層 
func (c *Context) String(code int, format string, values ...interface{}) {
    c.Render(code, render.String{Format: format, Data: values})
}

//對(duì)于json的處理
JSON struct {
    Data interface{}
}

//對(duì)于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)
    }
}

總結(jié)

這個(gè)看代碼的過程是在有目標(biāo)之后,按照官方文檔的例子缤至,一步一步的看的潮罪。然后再慢慢欣賞,這框架對(duì)于一些web框架常見的場(chǎng)景领斥,它是怎么處理嫉到。這框架的代碼量很少,而且寫得十分的優(yōu)雅,非常值得一看。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末郊霎,一起剝皮案震驚了整個(gè)濱河市饵婆,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌墩划,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異疫赎,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)碎节,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門捧搞,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人钓株,你說我怎么就攤上這事实牡。” “怎么了轴合?”我有些...
    開封第一講書人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵创坞,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我受葛,道長(zhǎng)题涨,這世上最難降的妖魔是什么偎谁? 我笑而不...
    開封第一講書人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮纲堵,結(jié)果婚禮上巡雨,老公的妹妹穿的比我還像新娘。我一直安慰自己席函,他們只是感情好铐望,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著茂附,像睡著了一般正蛙。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上营曼,一...
    開封第一講書人閱讀 49,166評(píng)論 1 284
  • 那天乒验,我揣著相機(jī)與錄音,去河邊找鬼蒂阱。 笑死锻全,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的录煤。 我是一名探鬼主播鳄厌,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼辐赞!你這毒婦竟也來了部翘?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤响委,失蹤者是張志新(化名)和其女友劉穎新思,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體赘风,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡夹囚,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了邀窃。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片荸哟。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖瞬捕,靈堂內(nèi)的尸體忽然破棺而出鞍历,到底是詐尸還是另有隱情,我是刑警寧澤肪虎,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布劣砍,位于F島的核電站,受9級(jí)特大地震影響扇救,放射性物質(zhì)發(fā)生泄漏刑枝。R本人自食惡果不足惜香嗓,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望装畅。 院中可真熱鬧靠娱,春花似錦、人聲如沸掠兄。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)蚂夕。三九已至苫费,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間双抽,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來泰國(guó)打工闲礼, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留牍汹,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓柬泽,卻偏偏與公主長(zhǎng)得像慎菲,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子锨并,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

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

  • 所謂框架 框架一直是敏捷開發(fā)中的利器露该,能讓開發(fā)者很快的上手并做出應(yīng)用,甚至有的時(shí)候第煮,脫離了框架解幼,一些開發(fā)者都不會(huì)寫...
    人世間閱讀 216,129評(píng)論 11 242
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)包警,斷路器撵摆,智...
    卡卡羅2017閱讀 134,601評(píng)論 18 139
  • 使用Gin構(gòu)建Go Web應(yīng)用程序和微服務(wù) 原文鏈接:https://semaphoreci.com/commun...
    devabel閱讀 15,863評(píng)論 2 28
  • 引用:https://github.com/WangZhechao/expross 1.簡(jiǎn)介 這篇文章是分析exp...
    宮若石閱讀 3,067評(píng)論 1 8
  • 【文學(xué)群·凌逍逸】/文 今天原本要寫的是關(guān)于“摔手機(jī)事件”的評(píng)價(jià)的壹瘟,借此討論道德高標(biāo)和每個(gè)人成為好人的機(jī)會(huì)鲫剿。 不過...
    凌逍逸閱讀 1,150評(píng)論 0 2