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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容