1. 程式人生 > >go web開發之url路由設計

go web開發之url路由設計

概述

最近在搞自己的go web開發框架, 反正也沒打算私藏, 所以現在先拿出url路由設計這塊來寫一篇部落格. 做過web開發的都知道, 一個好的url路由可以讓使用者瀏覽器的位址列總有規律可循, 可以讓我們開發的網站更容易讓搜尋引擎收錄, 可以讓我們開發者更加方便的MVC. 我們在使用其他web開發框架的時候, url路由肯定也會作為框架的一個重點功能或者說是一個宣傳”賣點”. 所以說, 一個web框架中url路由的地位還是非常重要的.

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

如何使用

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

package main

import (
    "./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 controller

import (
    "../app"
  "../funcs"
    "html/template"
)

type IndexController struct {
    app.App
}

func (i *IndexController) Index() {
  i.Data["name"
] = "qibin" i.Data["email"] = "[email protected]" //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 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的某一部分, 然後我們通過它來獲取到具體要執行拿個結構體. 那他的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開發的同學來說應該非常熟悉了, ServerHandler我們是通過一個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中還必須要儲存ResponseWriterRequest等資訊, 所以為了防止一次請求的資訊會被其他請求給重寫掉, 我們這裡選擇使用物件池, 在用的時候拿出來, 用完了之後進去, 每次使用前先將資訊重新整理, 這樣就避免了不用請求資訊會被重寫的錯誤.對於sync.Pool這裡簡單解釋一下, 還及得上面我們曾經給他的一個New欄位賦值嗎? 這裡面的邏輯就是, 當我們從這個pool中取的時候如果沒有就會到用New來新建一個, 因此這裡在可以保證Context唯一的前提下, 還能保證我們每次從pool中獲取總能拿到.

繼續看程式碼, 接下來我們就是通過findControllerInfo從url中解析出我們要執行的controllermethod的名字, 往下走, 我們通過反射來新建了一個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來說,這裡我們就是要去拿這個userinfo, 但是對於http://localhost:8080或者http://localhost:8080/user咋辦呢? 我們也會有預設的,

const (
    defController = "index"
    defMethod     = "Index"
)

到現在位置, 我們的url路由基本已經成型了, 不過還有幾個點我們還沒有射擊到, 例如上面經常看到的AppContext. 首先我們來看看這個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
}

這裡我們先簡單封裝一下, 僅僅儲存了ResponseWriterRequest, 每次請求的時候我們都會呼叫Config方法將新的ResponseWriterRequest儲存進去.

而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已經使用過了, 而WR方法純粹是為了方便獲取ResponseWriterRequest的, 下面的兩個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的一些實現方法. 在遇到問題時閱讀並理解別人的程式碼才是讀原始碼的正確方式.

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