go web開發之url路由設計

來源:互聯網
上載者:User
概述

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

最後我們通過一張運行截圖來結束這篇文章吧.

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.