概述
最近在搞自己的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)方法. 在遇到問題時閱讀并理解別人的代碼才是讀源碼的正確方式.