美文网首页
go web开发之url路由设计

go web开发之url路由设计

作者: golanger | 来源:发表于2017-10-13 14:54 被阅读0次

    概述

    最近在搞自己的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"] = "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 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开发的同学来说应该非常熟悉了,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 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, 每次请求的时候我们都会调用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的一些实现方法. 在遇到问题时阅读并理解别人的代码才是读源码的正确方式.

    相关文章

      网友评论

          本文标题:go web开发之url路由设计

          本文链接:https://www.haomeiwen.com/subject/hvpzyxtx.html