go web开发之url路由设计

版权声明:本文来自Loader's Blog,未经博主允许不得转载。 https://blog.csdn.net/qibin0506/article/details/52614290

概述

最近在搞自己的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的一些实现方法. 在遇到问题时阅读并理解别人的代码才是读源码的正确方式.

最后我们通过一张运行截图来结束这篇文章吧.

猜你喜欢

转载自blog.csdn.net/qibin0506/article/details/52614290