概述
最近在搞自己的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的一些实现方法. 在遇到问题时阅读并理解别人的代码才是读源码的正确方式.
最后我们通过一张运行截图来结束这篇文章吧.
有疑问加站长微信联系(非本文作者)
![](https://static.golangjob.cn/static/img/footer.png?imageView2/2/w/280)