httprouter框架簡析(一)

簡述

關(guān)于go的web開發(fā)框架冲茸,主要是兩大類屯阀。一類以beego為主缅帘,功能全,配置多难衰,大而全钦无;一類是以gin為主的,基于httprouter進(jìn)行開發(fā)盖袭,輕而快失暂。這個(gè)無所謂好壞,全看個(gè)人選擇鳄虱。今天主要說httprouter是如何實(shí)現(xiàn)弟塞。

  • httprouter支持參數(shù)類型的URL(/user/:id )也支持* 的通配符URL(/static/*file)。

  • httprouter使用radix tree(前綴樹)對請求的URL進(jìn)行管理拙已,最差時(shí)間復(fù)雜度為O(n)决记,n為URL長度。

  • httprouter對每種請求方法都會建立一個(gè)tree進(jìn)行管理倍踪。例如所有的GET請求會都用一個(gè)tree系宫,所有的POST用一個(gè)tree,所有的PUT一個(gè)tree......

  • 當(dāng)同時(shí)使用GET方式添加路由/ff/:id/ff/kk 時(shí)建车,會拋出panic: 'kk' in new path '/ff/kk' conflicts with existing wildcard ':id' in existing prefix '/ff/:id' 扩借。是因?yàn)樵贕ET的tree上面,前綴都是/ff的情況下缤至,參數(shù)類型的:id和具體的路由字段kk 沖突潮罪。

關(guān)于httprouter目錄

項(xiàng)目總共三個(gè)文件router.go、tree.go领斥、path.go错洁,其中router.go是主文件,路由的添加戒突、異常路由的處理等基本設(shè)置在此完成屯碴,調(diào)用httprouter.New() 返回一個(gè)Router結(jié)構(gòu)的指針。tree.go負(fù)責(zé)前綴樹的操作膊存,包括將路由添加到前綴樹中导而,從樹中查詢到URL對應(yīng)的handler。path.go主要包含CleanPath方法隔崎,返回一個(gè)符合規(guī)范的URL今艺。

關(guān)于router.go 中的 Router結(jié)構(gòu)

type Router struct {
  //前綴樹的結(jié)構(gòu)
  trees map[string]*node
  paramsPool sync.Pool
  maxParams  uint16
  //如果無法匹配到當(dāng)前路由,但是存在帶有(不帶)尾部斜杠的路由時(shí)爵卒,是否自動重定向
  //例如虚缎,如果請求了/foo/,但是只存在/foo的路由,那么將301重定向到 /foo
  RedirectTrailingSlash bool
  //是否修正路徑钓株,主要依靠path.go 的 CleanPath 方法
  RedirectFixedPath bool
  //如果無法匹配到當(dāng)前路由, 那么檢查是否有其他方法能否拼配到當(dāng)前的路由
  HandleMethodNotAllowed bool
  //是否允許路由自動拼配到options
  HandleOPTIONS bool
  //全局的options操作实牡,可以設(shè)置CORS時(shí)調(diào)用
  GlobalOPTIONS http.Handler
  globalAllowed string
  //當(dāng)沒有匹配到路由的時(shí)候, 執(zhí)行這個(gè)handler. 如果沒有配置,那么返回NotFound
  NotFound http.Handler
  //當(dāng)沒有匹配到路由并且HandleMethodNotAllowed=true的時(shí)候,這個(gè)函數(shù)被使用
  MethodNotAllowed http.Handler
  PanicHandler func(http.ResponseWriter, *http.Request, interface{})
}

這其中最重要的就是trees節(jié)點(diǎn)陌僵,路由的添加、查找都是通過這個(gè)操作完成创坞。

關(guān)于node的結(jié)構(gòu)

type node struct {
    //當(dāng)前node 的路徑
    path      string
    // children對應(yīng)的索引碗短,保存的是分裂分支的第一個(gè)字符
    indices   string
    //判斷子節(jié)點(diǎn)是否為通配節(jié)點(diǎn),(包括:param题涨、catchAll)
    wildChild bool
    //節(jié)點(diǎn)類型偎谁。
    //root: 第一個(gè)插入的節(jié)點(diǎn)
    //catchAll: 有* 匹配的節(jié)點(diǎn)
    //param:參數(shù)節(jié)點(diǎn) :id
    //static: 靜態(tài)節(jié)點(diǎn),前幾個(gè)剩下的都是靜態(tài)節(jié)點(diǎn)
    nType     nodeType
    // 優(yōu)先級
    priority  uint32
    //子節(jié)點(diǎn)
    children  []*node
    //當(dāng)前節(jié)點(diǎn)的處理函數(shù)
    handle    Handle
}

node示意圖

例如GET請求纲堵,/search/:id/info巡雨、/status/:id/info/user/getall

httprouter.png

關(guān)于addRoute


當(dāng)調(diào)用GET添加路由時(shí)席函,走的是Router的Handle方法鸯隅。同樣,post向挖、put蝌以、delete等也是一樣

而Handle最后走向addRoute。

addRoute 的基本思路就是先尋找路由的最長公共前綴何之,找到對應(yīng)的節(jié)點(diǎn)位置跟畅,然后調(diào)用insertChild,插入到對應(yīng)的node中。

  1. tree是一個(gè)空樹時(shí)溶推,那么當(dāng)前URL直接插入path徊件,nType為root
  2. tree不為空時(shí),如果匹配的最長前綴小于當(dāng)前節(jié)點(diǎn)path的長度蒜危,那么說明當(dāng)前節(jié)點(diǎn)需要進(jìn)行調(diào)整虱痕。最長公共前綴作為節(jié)點(diǎn)的path,原先節(jié)點(diǎn)成為最長公共前綴的子節(jié)點(diǎn)辐赞。
  3. tree不為空時(shí)部翘,如果匹配的最長前綴小于傳入的path的長度,說明需要在當(dāng)前節(jié)點(diǎn)下响委,添加一個(gè)新的子節(jié)點(diǎn)新思。當(dāng)然這里可能會有循環(huán)遞歸。
  4. tree不為空時(shí)赘风,如果匹配的最長前綴等于path夹囚,那么就是在此節(jié)點(diǎn)直接添加handler操作。

代碼說明

// addRoute adds a node with the given handle to the path.
// Not concurrency-safe!
func (n *node) addRoute(path string, handle Handle) {
    fullPath := path
    n.priority++

    // Empty tree
    if len(n.path) == 0 && len(n.indices) == 0 {
        n.insertChild(path, fullPath, handle)
        n.nType = root
        return
    }

walk:
    for {
        // Find the longest common prefix.
        // This also implies that the common prefix contains no ':' or '*'
        // since the existing key can't contain those chars.
        // 尋找傳入的路由與節(jié)點(diǎn)的最長公共前綴
        i := longestCommonPrefix(path, n.path)

        // Split edge--分割 邊際
        // 如果最長前綴的長度 < 節(jié)點(diǎn)長度
        if i < len(n.path) {
            //那么當(dāng)前節(jié)點(diǎn)的path就應(yīng)該是最長前綴邀窃,原先的節(jié)點(diǎn)是最長前綴的子節(jié)點(diǎn)
            child := node{
                //節(jié)點(diǎn)的路徑就是 當(dāng)前節(jié)點(diǎn)后段字符串
                path:      n.path[i:],
                wildChild: n.wildChild,
                nType:     static,
                indices:   n.indices,
                children:  n.children,
                handle:    n.handle,
                priority:  n.priority - 1,
            }
            //剛分裂的節(jié)點(diǎn)荸哟,作為新節(jié)點(diǎn)的子節(jié)點(diǎn)
            n.children = []*node{&child}
            // []byte for proper unicode char conversion, see #65
            n.indices = string([]byte{n.path[i]})
            n.path = path[:i]
            n.handle = nil
            n.wildChild = false
        }

        // Make new node a child of this node    ----為當(dāng)前節(jié)點(diǎn)創(chuàng)建一個(gè)新的子節(jié)點(diǎn)
        //如果最長前綴 < 路徑的長度  
        //除了tree為空的情況外,只有在這個(gè)判斷下才會走 insertChild ,創(chuàng)建新節(jié)點(diǎn)
        if i < len(path) {
            path = path[i:]

            //如果子節(jié)點(diǎn)是參數(shù)節(jié)點(diǎn)
            if n.wildChild {
                n = n.children[0]
                n.priority++
                
                // Check if the wildcard matches  
                // 如果子節(jié)點(diǎn)是通配節(jié)點(diǎn)鞍历,需要驗(yàn)證合法性
                if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
                    // Adding a child to a catchAll is not possible   
                    n.nType != catchAll &&
                    // Check for longer wildcard, e.g. :name and :names  
                    (len(n.path) >= len(path) || path[len(n.path)] == '/') {
                    continue walk
                } else {
                    // Wildcard conflict
                    // 通配符沖突
                    pathSeg := path
                    if n.nType != catchAll {
                        // SplitN 根據(jù)/分割字符串舵抹,最多2個(gè)
                        pathSeg = strings.SplitN(pathSeg, "/", 2)[0]
                    }
                    prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
                    panic("'" + pathSeg +
                        "' in new path '" + fullPath +
                        "' conflicts with existing wildcard '" + n.path +
                        "' in existing prefix '" + prefix +
                        "'")
                }
            }

            idxc := path[0]

            // '/' after param          --
            if n.nType == param && idxc == '/' && len(n.children) == 1 {
                n = n.children[0]
                n.priority++
                continue walk
            }

            // Check if a child with the next path byte exists
            //判斷當(dāng)前節(jié)點(diǎn)的 indices 索引中是否存在當(dāng)前path的首字母
            for i, c := range []byte(n.indices) {
                if c == idxc {
                    i = n.incrementChildPrio(i)
                    n = n.children[i]
                    continue walk
                }
            }

            // Otherwise insert it   ---否則直接插入
            if idxc != ':' && idxc != '*' {
                // []byte for proper unicode char conversion, see #65
                n.indices += string([]byte{idxc})
                child := &node{}
                n.children = append(n.children, child)
                n.incrementChildPrio(len(n.indices) - 1)
                n = child
            }
            n.insertChild(path, fullPath, handle)
            return
        }

        // Otherwise add handle to current node
        //如果最長前綴 == 傳入的path,就直接在這個(gè)節(jié)點(diǎn)添加handle
        if n.handle != nil {
            panic("a handle is already registered for path '" + fullPath + "'")
        }
        n.handle = handle
        return
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末堰燎,一起剝皮案震驚了整個(gè)濱河市掏父,隨后出現(xiàn)的幾起案子笋轨,更是在濱河造成了極大的恐慌秆剪,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,816評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件爵政,死亡現(xiàn)場離奇詭異仅讽,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)钾挟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,729評論 3 385
  • 文/潘曉璐 我一進(jìn)店門洁灵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人掺出,你說我怎么就攤上這事徽千。” “怎么了汤锨?”我有些...
    開封第一講書人閱讀 158,300評論 0 348
  • 文/不壞的土叔 我叫張陵双抽,是天一觀的道長。 經(jīng)常有香客問我闲礼,道長牍汹,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,780評論 1 285
  • 正文 為了忘掉前任柬泽,我火速辦了婚禮慎菲,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘锨并。我一直安慰自己露该,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,890評論 6 385
  • 文/花漫 我一把揭開白布第煮。 她就那樣靜靜地躺著有决,像睡著了一般。 火紅的嫁衣襯著肌膚如雪空盼。 梳的紋絲不亂的頭發(fā)上书幕,一...
    開封第一講書人閱讀 50,084評論 1 291
  • 那天,我揣著相機(jī)與錄音揽趾,去河邊找鬼台汇。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的苟呐。 我是一名探鬼主播痒芝,決...
    沈念sama閱讀 39,151評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼牵素!你這毒婦竟也來了严衬?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,912評論 0 268
  • 序言:老撾萬榮一對情侶失蹤笆呆,失蹤者是張志新(化名)和其女友劉穎请琳,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體赠幕,經(jīng)...
    沈念sama閱讀 44,355評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡俄精,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,666評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了榕堰。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片竖慧。...
    茶點(diǎn)故事閱讀 38,809評論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖逆屡,靈堂內(nèi)的尸體忽然破棺而出圾旨,到底是詐尸還是另有隱情,我是刑警寧澤魏蔗,帶...
    沈念sama閱讀 34,504評論 4 334
  • 正文 年R本政府宣布砍的,位于F島的核電站,受9級特大地震影響沫勿,放射性物質(zhì)發(fā)生泄漏挨约。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,150評論 3 317
  • 文/蒙蒙 一产雹、第九天 我趴在偏房一處隱蔽的房頂上張望诫惭。 院中可真熱鬧,春花似錦蔓挖、人聲如沸夕土。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽怨绣。三九已至,卻和暖如春拷获,著一層夾襖步出監(jiān)牢的瞬間篮撑,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,121評論 1 267
  • 我被黑心中介騙來泰國打工匆瓜, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留赢笨,地道東北人未蝌。 一個(gè)月前我還...
    沈念sama閱讀 46,628評論 2 362
  • 正文 我出身青樓,卻偏偏與公主長得像茧妒,于是被迫代替她去往敵國和親萧吠。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,724評論 2 351

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