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