go web開發(fā)之url路由設計

概述

最近在搞自己的go web開發(fā)框架, 反正也沒打算私藏, 所以現(xiàn)在先拿出url路由設計這塊來寫一篇博客. 做過web開發(fā)的都知道,

一個好的url路由可以讓用戶瀏覽器的地址欄總有規(guī)律可循, 可以讓我們開發(fā)的網(wǎng)站更容易讓搜索引擎收錄, 可以讓我們開發(fā)者更加方便的MVC.

我們在使用其他web開發(fā)框架的時候, url路由肯定也會作為框架的一個重點功能或者說是一個宣傳”賣點”. 所以說,

一個web框架中url路由的地位還是非常重要的.

回到go web開發(fā)中, 那如何用go來實現(xiàn)一個url路由功能呢? 實現(xiàn)后代碼如何書寫呢? 下面我們就來一步步的去實現(xiàn)一個簡單的url路由功能.

如何使用

在我們學習如何實現(xiàn)之前, 肯定是要先看看如何使用的. 其實使用起來很簡單, 因為我之前寫過一個PHP的web開發(fā)框架, 所以我們的路由部分的使用像極了PHP(ThinkPHP). 來看看代碼吧.

package?main

import?(????

"./app"

"./controller"

)

func?main()?{

app.Static["/static"]?=?"./js"

app.AutoRouter(&controller.IndexController{})

app.RunOn(":8080")

}

三行代碼, 第一行的作用大家都應該清楚, 就是去serve一些靜態(tài)文件(例如js, css等文件), 第二行代碼是去注冊一個Controller, 這行代碼在PHP是沒有的, 畢竟PHP是動態(tài)語言, 一個__autoload就可以完成類的加載, 而go作為靜態(tài)語言沒有這項特性, 所以我們還是需要手工注冊的(思考一下, 這里是不是可以想java一樣放到配置文件中呢? 這個功能留到以后優(yōu)化的時候添加吧.) 還有最后一行代碼沒說, 其實就是啟動server了, 這里我們監(jiān)聽了8080端口.

上面的代碼很簡單, 我們來看看那個IndexController怎么寫的.

package?controller

import?(???

?"../app"

"../funcs"

"html/template"

)

type?IndexController?struct?{

app.App

}

func?(i?*IndexController)?Index()?{

i.Data["name"]?=?"qibin"

i.Data["email"]?=?"qibin0506@gmail.com"

//i.Display("./view/info.tpl",?"./view/header.tpl",?"./view/footer.tpl")

i.DisplayWithFuncs(template.FuncMap{"look":?funcs.Lookup},?"./view/info.tpl",?"./view/header.tpl",?"./view/footer.tpl")

}

首先我們定義一個結構體, 這個結構體匿名組合了App這個結構體(用面向?qū)ο蟮脑捳f就是繼承了), 然我們給他定義了一個Index方法, 這里面具體干了啥我們先不用去關心. 那怎么訪問到呢? 現(xiàn)在運行代碼, 在瀏覽器輸入http://localhost:8080或者輸入http://localhost:8080/index/index就可以看到我們在Index方法里輸出的內(nèi)容了, 具體怎么做到的, 其實這完全是url路由的功勞, 下面我們就開始著手準備設計這么一個url路由功能.

url路由的設計

上面的AutoRouter看起來很神奇,具體干了啥呢? 我們先來看看這個注冊路由的功能是如何實現(xiàn)的吧.

package?app

import?(???

?"reflect"

"strings"

)

var?mapping?map[string]reflect.Type?=?make(map[string]reflect.Type)

func?router(pattern?string,?t?reflect.Type)?{

mapping[strings.ToLower(pattern)]?=?t

}func?Router(pattern?string,?app?IApp)?{

refV?:=?reflect.ValueOf(app)

refT?:=?reflect.Indirect(refV).Type()

router(pattern,?refT)

}

func?AutoRouter(app?IApp)?{

refV?:=?reflect.ValueOf(app)

refT?:=?reflect.Indirect(refV).Type()

refName?:=?strings.TrimSuffix(strings.ToLower(refT.Name()),?"controller")

router(refName,?refT)

}

首先我們定義了一個map變量, 他的key是一個string類型, 我們猜想肯定是我們在瀏覽器中輸入的那個url的某一部分, 然后我們通過它來獲取到具體要執(zhí)行拿個結構體. 那他的value呢? 一個reflect.Type是干嘛的? 先別著急, 我們來看看AutoRouter的實現(xiàn)代碼就明白了. 在AutoRouter里, 首先我們用reflect.ValueOf來獲取到我們注冊的那個結構體的Value, 緊接著我們又獲取了它的Type, 最后我們將這一對string,Type放到了map了. 可是這里的代碼僅僅是解釋了怎么注冊進去的, 而沒有解釋為什么要保存Type啊, 這里偷偷告訴你, 其實對于每次訪問, 我們找到對應的Controller后并不是也一定不可能是直接調(diào)用這個結構體上的方法, 而是通過反射新建一個實例去調(diào)用. 具體的代碼我們稍后會說到.

到現(xiàn)在為止, 我們的路由就算注冊成功了, 雖然我們對于保存Type還寸有一定的疑慮. 下面我們就開始從RunOn函數(shù)開始慢慢的來看它是如何根據(jù)這個路由注冊表來找到對應的Controller及其方法的.

首先來看看RunOn的代碼.

func?RunOn(port?string)?{

server?:=?&http.Server{

Handler:?newHandler(),

Addr:????port,

}

log.Fatal(server.ListenAndServe())

}

這里面的代碼也很簡單, 對于熟悉go web開發(fā)的同學來說應該非常熟悉了,Server的Handler我們是通過一個newHandler函數(shù)來返回的, 這個newHandler做了啥呢?

func?newHandler()?*handler?{

h?:=?&handler{}

h.p.New?=?func()?interface{}?{????????return?&Context{}

}????return?h

}

首先構造了一個handler, 然后又給handler里的一個sync.Pool做了賦值, 這個東西是干嘛的, 我們稍后會詳細說到, 下面我們就來安心的看這個handler結構體如何設計的.

type?handler?struct?{

p?sync.Pool

}

很簡單, 對于p上面說了, 在下面我們會詳細說到, 對于handler我們相信它肯定會有一個方法名叫ServeHTTP, 來看看吧.

func?(h?*handler)?ServeHTTP(w?http.ResponseWriter,?r?*http.Request)?{????if?serveStatic(w,?r)?{????????return

}

ctx?:=?h.p.Get().(*Context)????defer?h.p.Put(ctx)

ctx.Config(w,?r)

controllerName,?methodName?:=?h.findControllerInfo(r)

controllerT,?ok?:=?mapping[controllerName]????if?!ok?{

http.NotFound(w,?r)????????return

}

refV?:=?reflect.New(controllerT)

method?:=?refV.MethodByName(methodName)????if?!method.IsValid()?{

http.NotFound(w,?r)????????return

}

controller?:=?refV.Interface().(IApp)

controller.Init(ctx)

method.Call(nil)

}

這里面的代碼其實就是我們路由設計的核心代碼了, 下面我們詳細來看一下這里面的代碼如何實現(xiàn)的. 前三行代碼是我們對于靜態(tài)文件的支持.

接下來我們就用到了sync.Pool, 首先我們從里面拿出一個Context, 并在這個方法執(zhí)行完畢后將這個Context放進去, 這樣做是什么目的呢? 其實我們的網(wǎng)站并不是單行的, 所以這里的ServeHTTP并不是只為一個用戶使用, 而在咱們的Controller中還必須要保存ResponseWriter和Request等信息, 所以為了防止一次請求的信息會被其他請求給重寫掉, 我們這里選擇使用對象池, 在用的時候拿出來, 用完了之后進去, 每次使用前先將信息刷新, 這樣就避免了不用請求信息會被重寫的錯誤.對于sync.Pool這里簡單解釋一下, 還及得上面我們曾經(jīng)給他的一個New字段賦值嗎? 這里面的邏輯就是, 當我們從這個pool中取的時候如果沒有就會到用New來新建一個, 因此這里在可以保證Context唯一的前提下, 還能保證我們每次從pool中獲取總能拿到.

繼續(xù)看代碼, 接下來我們就是通過findControllerInfo從url中解析出我們要執(zhí)行的controller和method的名字, 往下走, 我們通過反射來新建了一個controller的對象, 并通過MethodByName來獲取到要執(zhí)行的方法.具體代碼:

refV?:=?reflect.New(controllerT)

method?:=?refV.MethodByName(methodName)

這里就解釋了, 上面為什么要保存reflect.Type. 最后我們將Context設置給這個Controller,并且調(diào)用我們找到的那個方法. 大體的url路由就這樣,主要是通過go的反射機制來找到要執(zhí)行的結構體和具體要執(zhí)行到的那個方法, 然后調(diào)用就可以了. 不過,這其中我們還有一個findControllerInfo還沒有說到, 它的實現(xiàn)就相對簡單, 就是通過url來找到controller和我們要執(zhí)行的方法的名稱. 來看一下代碼:

func?(h?*handler)?findControllerInfo(r?*http.Request)?(string,?string)?{

path?:=?r.URL.Path????if?strings.HasSuffix(path,?"/")?{

path?=?strings.TrimSuffix(path,?"/")

}

pathInfo?:=?strings.Split(path,?"/")

controllerName?:=?defController????if?len(pathInfo)?>?1?{

controllerName?=?pathInfo[1]

}

methodName?:=?defMethod????if?len(pathInfo)?>?2?{

methodName?=?strings.Title(strings.ToLower(pathInfo[2]))

}????return?controllerName,?methodName

}

這里首先我們拿到url中的pathInfo, 例如對于請求http://localhost:8080/user/info來說,這里我們就是要去拿這個user和info, 但是對于http://localhost:8080或者http://localhost:8080/user咋辦呢? 我們也會有默認的,

const?(

defController?=?"index"

defMethod?????=?"Index")

到現(xiàn)在位置, 我們的url路由基本已經(jīng)成型了, 不過還有幾個點我們還沒有射擊到, 例如上面經(jīng)常看到的App和Context. 首先我們來看看這個Context吧,這個Context是啥? 其實就是我們對請求信息的簡單封裝.

package?app

import?(????"net/http")

type?IContext?interface?{

Config(w?http.ResponseWriter,?r?*http.Request)

}

type?Context?struct?{

w?http.ResponseWriter

r?*http.Request

}

func?(c?*Context)?Config(w?http.ResponseWriter,?r?*http.Request)?{

c.w?=?w

c.r?=?r

}

這里我們先簡單封裝一下, 僅僅保存了ResponseWriter和Request, 每次請求的時候我們都會調(diào)用Config方法將新的ResponseWriter和Request保存進去.

而App呢? 設計起來就更加靈活了, 除了幾個在handler里用到的方法, 基本都是”臨場發(fā)揮的”.

type?IApp?interface?{

Init(ctx?*Context)

W()?http.ResponseWriter

R()?*http.Request

Display(tpls?...string)

DisplayWithFuncs(funcs?template.FuncMap,?tpls?...string)

}

這個接口里的方法大家應該都猜到了,Init方法我們在上面的ServeHTTP已經(jīng)使用過了, 而W和R方法純粹是為了方便獲取ResponseWriter和Request的, 下面的兩個Display方法這里也不多說了, 就是封裝了go原生的模板加載機制. 來看看App是如何實現(xiàn)這個接口的吧.

type?App?struct?{

ctx??*Context

Data?map[string]interface{}

}func?(a?*App)?Init(ctx?*Context)?{

a.ctx?=?ctx

a.Data?=?make(map[string]interface{})

}func?(a?*App)?W()?http.ResponseWriter?{????return?a.ctx.w

}func?(a?*App)?R()?*http.Request?{????return?a.ctx.r

}func?(a?*App)?Display(tpls?...string)?{????if?len(tpls)?==?0?{????????return

}

name?:=?filepath.Base(tpls[0])

t?:=?template.Must(template.ParseFiles(tpls...))

t.ExecuteTemplate(a.W(),?name,?a.Data)

}func?(a?*App)?DisplayWithFuncs(funcs?template.FuncMap,?tpls?...string)?{????if?len(tpls)?==?0?{????????return

}

name?:=?filepath.Base(tpls[0])

t?:=?template.Must(template.New(name).Funcs(funcs).ParseFiles(tpls...))

t.ExecuteTemplate(a.W(),?name,?a.Data)

}

ok, 該說的上面都說了, 最后我們還有一點沒看到的就是靜態(tài)文件的支持, 這里也很簡單.

var?Static?map[string]string?=?make(map[string]string)

func?serveStatic(w?http.ResponseWriter,?r?*http.Request)?bool?{???

?for?prefix,?static?:=?range?Static?{???????

?if?strings.HasPrefix(r.URL.Path,?prefix)?{

file?:=?static?+?r.URL.Path[len(prefix):]

http.ServeFile(w,?r,?file)???????????

?return?true

}

}????

return?false

}

到現(xiàn)在為止, 我們的一個簡單的url路由就實現(xiàn)了, 但是我們的這個實現(xiàn)還不完善, 例如自定義路由規(guī)則還不支持, 對于PathInfo里的參數(shù)我們還沒有獲取, 這些可以在完善階段完成. 在設計該路由的過程中充分的參考了beego的一些實現(xiàn)方法. 在遇到問題時閱讀并理解別人的代碼才是讀源碼的正確方式.

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末伤靠,一起剝皮案震驚了整個濱河市漩仙,隨后出現(xiàn)的幾起案子操禀,更是在濱河造成了極大的恐慌锚沸,老刑警劉巖第晰,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件碱茁,死亡現(xiàn)場離奇詭異裸卫,居然都是意外死亡,警方通過查閱死者的電腦和手機纽竣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門墓贿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蜓氨,你說我怎么就攤上這事聋袋。” “怎么了穴吹?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵幽勒,是天一觀的道長。 經(jīng)常有香客問我港令,道長啥容,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任顷霹,我火速辦了婚禮咪惠,結果婚禮上,老公的妹妹穿的比我還像新娘淋淀。我一直安慰自己遥昧,他們只是感情好,可當我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著渠鸽,像睡著了一般叫乌。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上徽缚,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天憨奸,我揣著相機與錄音,去河邊找鬼凿试。 笑死排宰,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的那婉。 我是一名探鬼主播板甘,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼详炬!你這毒婦竟也來了盐类?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤呛谜,失蹤者是張志新(化名)和其女友劉穎在跳,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體隐岛,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡猫妙,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了聚凹。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片割坠。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖妒牙,靈堂內(nèi)的尸體忽然破棺而出彼哼,到底是詐尸還是另有隱情,我是刑警寧澤湘今,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布敢朱,位于F島的核電站,受9級特大地震影響象浑,放射性物質(zhì)發(fā)生泄漏蔫饰。R本人自食惡果不足惜琅豆,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一愉豺、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧茫因,春花似錦蚪拦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽盛嘿。三九已至,卻和暖如春括袒,著一層夾襖步出監(jiān)牢的瞬間次兆,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工锹锰, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留芥炭,地道東北人。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓恃慧,卻偏偏與公主長得像园蝠,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子痢士,可洞房花燭夜當晚...
    茶點故事閱讀 42,762評論 2 345

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

  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統(tǒng)中一些常見模式的工具(例如配置管理彪薛,服務發(fā)現(xiàn),斷路器怠蹂,智...
    卡卡羅2017閱讀 134,599評論 18 139
  • ¥開啟¥ 【iAPP實現(xiàn)進入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個線程善延,因...
    小菜c閱讀 6,358評論 0 17
  • 22年12月更新:個人網(wǎng)站關停,如果仍舊對舊教程有興趣參考 Github 的markdown內(nèi)容[https://...
    tangyefei閱讀 35,160評論 22 257
  • 從三月份找實習到現(xiàn)在褥蚯,面了一些公司挚冤,掛了不少,但最終還是拿到小米赞庶、百度训挡、阿里、京東歧强、新浪澜薄、CVTE、樂視家的研發(fā)崗...
    時芥藍閱讀 42,184評論 11 349
  • 作者/Author:蘇近之 地點/Address:浙江省杭州市小和山浙江科技學院 時間/Time:2016年05月...
    IYANYU閱讀 181評論 0 0