概述
最近在搞自己的go web開發架構, 反正也沒打算私藏, 所以現在先拿出url路由設計這塊來寫一篇部落格. 做過web開發的都知道, 一個好的url路由可以讓使用者瀏覽器的地址欄總有規律可循, 可以讓我們開發的網站更容易讓搜尋引擎收錄, 可以讓我們開發人員更加方便的MVC. 我們在使用其他web開發架構的時候, url路由肯定也會作為架構的一個重點功能或者說是一個宣傳”賣點”. 所以說, 一個web架構中url路由的地位還是非常重要的.
回到go web開發中, 那如何用go來實現一個url路由功能呢? 實現後代碼如何書寫呢? 下面我們就來一步步的去實現一個簡單的url路由功能. 如何使用
在我們學習如何?之前, 肯定是要先看看如何使用的. 其實使用起來很簡單, 因為我之前寫過一個PHP的web開發架構, 所以我們的路由部分的使用像極了PHP(ThinkPHP). 來看看代碼吧.
package mainimport ( "./app" "./controller")func main() { app.Static["/static"] = "./js" app.AutoRouter(&controller.IndexController{}) app.RunOn(":8080")}
三行代碼, 第一行的作用大家都應該清楚, 就是去serve一些靜態檔案(例如js, css等檔案), 第二行代碼是去註冊一個Controller, 這行代碼在PHP是沒有的, 畢竟PHP是動態語言, 一個__autoload就可以完成類的載入, 而go作為靜態語言沒有這項特性, 所以我們還是需要手工註冊的(思考一下, 這裡是不是可以想java一樣放到設定檔中呢? 這個功能留到以後最佳化的時候添加吧.) 還有最後一行代碼沒說, 其實就是啟動server了, 這裡我們監聽了8080連接埠.
上面的代碼很簡單, 我們來看看那個IndexController怎麼寫的.
package controllerimport ( "../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這個結構體(用物件導向的話說就是繼承了), 然我們給他定義了一個Index方法, 這裡面具體幹了啥我們先不用去關心. 那怎麼訪問到呢? 現在運行代碼, 在瀏覽器輸入http://localhost:8080或者輸入http://localhost:8080/index/index就可以看到我們在Index方法裡輸出的內容了, 具體怎麼做到的, 其實這完全是url路由的功勞, 下面我們就開始著手準備設計這麼一個url路由功能. url路由的設計
上面的AutoRouter看起來很神奇,具體幹了啥呢? 我們先來看看這個註冊路由的功能是如何?的吧.
package appimport ( "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的某一部分, 然後我們通過它來擷取到具體要執行拿個結構體. 那他的value呢? 一個reflect.Type是幹嘛的? 先別著急, 我們來看看AutoRouter的實現代碼就明白了. 在AutoRouter裡, 首先我們用reflect.ValueOf來擷取到我們註冊的那個結構體的Value, 緊接著我們又擷取了它的Type, 最後我們將這一對string,Type放到了map了. 可是這裡的代碼僅僅是解釋了怎麼註冊進去的, 而沒有解釋為什麼要儲存Type啊, 這裡偷偷告訴你, 其實對於每次訪問, 我們找到對應的Controller後並不是也一定不可能是直接調用這個結構體上的方法, 而是通過反射建立一個執行個體去調用. 具體的代碼我們稍後會說到.
到現在為止, 我們的路由就算註冊成功了, 雖然我們對於儲存Type還寸有一定的疑慮. 下面我們就開始從RunOn函數開始慢慢的來看它是如何根據這個路由註冊表來找到對應的Controller及其方法的.
首先來看看RunOn的代碼.
func RunOn(port string) { server := &http.Server{ Handler: newHandler(), Addr: port, } log.Fatal(server.ListenAndServe())}
這裡面的代碼也很簡單, 對於熟悉go web開發的同學來說應該非常熟悉了, Server的Handler我們是通過一個newHandler函數來返回的, 這個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)}
這裡面的代碼其實就是我們路由設計的核心代碼了, 下面我們詳細來看一下這裡面的代碼如何?的. 前三行代碼是我們對於靜態檔案的支援.
接下來我們就用到了sync.Pool, 首先我們從裡面拿出一個Context, 並在這個方法執行完畢後將這個Context放進去, 這樣做是什麼目的呢? 其實我們的網站並不是單行的, 所以這裡的ServeHTTP並不是只為一個使用者使用, 而在咱們的Controller中還必須要儲存ResponseWriter和Request等資訊, 所以為了防止一次請求的資訊會被其他請求給重寫掉, 我們這裡選擇使用對象池, 在用的時候拿出來, 用完了之後進去, 每次使用前先將資訊重新整理, 這樣就避免了不用請求資訊會被重寫的錯誤.對於sync.Pool這裡簡單解釋一下, 還及得上面我們曾經給他的一個New欄位賦值嗎? 這裡面的邏輯就是, 當我們從這個pool中取的時候如果沒有就會到用New來建立一個, 因此這裡在可以保證Context唯一的前提下, 還能保證我們每次從pool中擷取總能拿到.
繼續看代碼, 接下來我們就是通過findControllerInfo從url中解析出我們要執行的controller和method的名字, 往下走, 我們通過反射來建立了一個controller的對象, 並通過MethodByName來擷取到要執行的方法.具體代碼:
refV := reflect.New(controllerT)method := refV.MethodByName(methodName)
這裡就解釋了, 上面為什麼要儲存reflect.Type. 最後我們將Context設定給這個Controller,並且調用我們找到的那個方法. 大體的url路由就這樣,主要是通過go的反射機制來找到要執行的結構體和具體要執行到的那個方法, 然後調用就可以了. 不過,這其中我們還有一個findControllerInfo還沒有說到, 它的實現就相對簡單, 就是通過url來找到controller和我們要執行的方法的名稱. 來看一下代碼:
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")
到現在位置, 我們的url路由基本已經成型了, 不過還有幾個點我們還沒有射擊到, 例如上面經常看到的App和Context. 首先我們來看看這個Context吧,這個Context是啥? 其實就是我們對請求資訊的簡單封裝.
package appimport ( "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, 每次請求的時候我們都會調用Config方法將新的ResponseWriter和Request儲存進去.
而App呢? 設計起來就更加靈活了, 除了幾個在handler裡用到的方法, 基本都是”臨場發揮的”.
type IApp interface { Init(ctx *Context) W() http.ResponseWriter R() *http.Request Display(tpls ...string) DisplayWithFuncs(funcs template.FuncMap, tpls ...string)}
這個介面裡的方法大家應該都猜到了, Init方法我們在上面的ServeHTTP已經使用過了, 而W和R方法純粹是為了方便擷取ResponseWriter和Request的, 下面的兩個Display方法這裡也不多說了, 就是封裝了go原生的模板載入機制. 來看看App是如何?這個介面的吧.
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, 該說的上面都說了, 最後我們還有一點沒看到的就是靜態檔案的支援, 這裡也很簡單.
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}
到現在為止, 我們的一個簡單的url路由就實現了, 但是我們的這個實現還不完善, 例如自訂路由規則還不支援, 對於PathInfo裡的參數我們還沒有擷取, 這些可以在完善階段完成. 在設計該路由的過程中充分的參考了beego的一些實現方法. 在遇到問題時閱讀並理解別人的代碼才是讀源碼的正確方式.
最後我們通過一張運行截圖來結束這篇文章吧.