基于 gorilla/mux 包实现路由定义和请求分发:进阶使用

上篇教程我们介绍了 gorilla/mux 路由的基本使用,这篇教程继续介绍它的更多匹配规则,实际上,它可能是一个比 Laravel 路由更加强大的存在。

限定请求方法

类似 Laravel 路由可以通过 Route::getRoute::post 这种方式来限定 HTTP 请求方法,gorilla/mux 支持通过 Methods 方法来限定请求方法,我们可以通过链式调用将其应用到上篇教程定义的基础路由规则上:

r := mux.NewRouter()
r.HandleFunc("/hello/{name:[a-z]+}", sayHelloWorld).Methods("GET", "POST")
r.Handle("/zh/hello/{name}", &HelloWorldHandler{}).Methods("GET")
log.Fatal(http.ListenAndServe(":8080", r))

下面我们通过 cURL 在命令行测试路由访问,当我们试图对 http://localhost:8080/zh/hello/golang 发起 POST 请求时,结果为空,表示不支持该方法:

-w725

路由前缀

和 Laravel 路由一样,gorilla/mux 路由也支持路由前缀

r.PathPrefix("/hello").HandlerFunc(sayHelloWorld)

不过,路由前缀通常不会单独使用,而是和子路由结合使用,从而实现对路由的分组。

域名匹配

此外,gorilla/mux 路由还支持域名匹配,这和 Laravel 路由的子域名路由功能非常相似,只需在原来的路由规则基础上追加 Host 方法调用并指定域名即可:

r.HandleFunc("/hello/{name:[a-z]+}",    sayHelloWorld).Methods("GET").Host("goweb.test")

这样一来,只有当请求 URL 的域名为 goweb.test 时才会匹配到对应路由映射:

-w682

当然,传入的域名参数值为子域名时,就是子域名匹配了:

r.Handle("/zh/hello/{name}", &HelloWorldHandler{}).Methods("GET").Host("zh.goweb.test")

效果和上面一样:

-w648

为了保证上述测试成功,需要在本地 hosts 文件中添加相应的域名映射:

127.0.0.1 goweb.test
127.0.0.1 zh.goweb.test

限定 Scheme

gorilla/mux 路由支持通过 Schemes 方法设置 Scheme 匹配:

r.Handle("/zh/hello/{name}", &HelloWorldHandler{}).Methods("GET").Host("zh.goweb.test").Schemes("https")

这样一来,只有 HTTPS 请求才能访问对应路由,对于 HTTP 请求,会返回 404 错误:

-w641

限定请求参数

接下来的几个路由匹配规则是 Laravel 不支持的,我们可以在 gorilla/mux 路由定义中通过 Headers 方法设置请求头匹配,比如下面这个示例,请求头必须包含 X-Requested-With 并且值为 XMLHttpRequest 才可以访问指定路由 /request/header

r.HandleFunc("/request/header", func(w http.ResponseWriter, r *http.Request) {
    header := "X-Requested-With"
    fmt.Fprintf(w, "包含指定请求头[%s=%s]", header, r.Header[header])
}).Headers("X-Requested-With", "XMLHttpRequest")

这样做的意义是限定客户端只能通过 Ajax 请求访问该路由,测试命令如下:

-w882

除了请求头之外,还可以通过 Queries 方法限定查询字符串,比如下面这个示例,查询字符串必须包含 token 且值为 test 才可以匹配到给定路由 /query/string

r.HandleFunc("/query/string", func(w http.ResponseWriter, r *http.Request) {
    query := "token"
    fmt.Fprintf(w, "包含指定查询字符串[%s=%s]", query, r.FormValue(query))
}).Queries("token", "test")

这在一些需要访问令牌的请求中非常有用,可以规避掉无效的请求。测试命令如下:

-w721

在 Laravel 中,可以通过中间件完成类似的功能,不过 gorilla/mux 可以更早地规避这种非法请求。

自定义匹配规则

最后,gorilla/mux 路由支持通过 MatcherFunc 方法自定义路由匹配规则,在该方法中,可以获取到请求实例 request,这样我们就可以拿到所有的用户请求信息,并对其进行判断,符合我们预期的请求才能匹配并访问该方法应用到的路由。

比如下面这个示例,我们限定只有来自 https://xueyuanjun.com 域名的请求才可以匹配到 /custom/matcher 路由:

r.HandleFunc("/custom/matcher", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "请求来自指定域名: %s", r.Referer())
}).MatcherFunc(func(request *http.Request, match *mux.RouteMatch) bool {
    return request.Referer() == "https://xueyuanjun.com"
})

如果不是的话,会返回 404 响应:

-w845

路由分组

作为路由匹配进阶使用教程的收尾,我们来看下如何在 gorilla/mux 路由中实现路由分组和命名,以及根据命名路由生成对应的 URL。

首先来看路由分组,gorilla/mux 没有直接提供类似路由分组的术语,这里我们借鉴 Laravel 路由的表述,以方便理解。

gorilla/mux 中,可以基于子路由器(Subrouter)来实现路由分组的功能,具体使用时,还可以借助前面介绍的路由前缀和域名匹配来对不同分组路由进行特性区分。

下面,我们以文章增删改查为例,将文章相关路由规则划分到路由前缀为 /posts 的子路由中:

func listPosts(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "文章列表")
}

func createPost(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "发布文章")
}

func updatePost(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "修改文章")
}

func deletePost(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "删除文章")
}

func showPost(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "文章详情")
}

...

// 路由分组(基于子路由+路径前缀)
postRouter := r.PathPrefix("/posts").Subrouter()
postRouter.HandleFunc("/", listPosts).Methods("GET")
postRouter.HandleFunc("/create", createPost).Methods("POST")
postRouter.HandleFunc("/update", updatePost).Methods("PUT")
postRouter.HandleFunc("/delete", deletePost).Methods("DELETE")
postRouter.HandleFunc("/show", showPost).Methods("GET")

这样,/posts 前缀会应用到后面所有基于 postRouter 子路由定义的路由规则上,并且针对不同的操作,我们还限定了对应的请求方法,我们可以像这样测试上述路由的访问:

-w680

如果上述路由是管理后台路由,还可以结合子域名做进一步划分:

postRouter := r.PathPrefix("/posts").Host("admin.goweb.test").Subrouter()

这样一来,只有域名为 admin.goweb.test 时才可以访问对应路由,提高了安全性:

-w754

路由命名

最后我们来看一下 gorilla/mux 中的路由命名,和 Laravel 路由命名一样,也是通过 Name 方法在路由规则中指定:

postRouter := r.PathPrefix("/posts").Subrouter()
postRouter.HandleFunc("/", listPosts).Methods("GET").Name("posts.index")
postRouter.HandleFunc("/create", createPost).Methods("POST").Name("posts.create")
postRouter.HandleFunc("/update", updatePost).Methods("PUT").Name("posts.update")
postRouter.HandleFunc("/delete", deletePost).Methods("DELETE").Name("posts.delete")
postRouter.HandleFunc("/show/{id:[0-9]+}", showPost).Methods("GET").Name("posts.show")

然后我们可以像下面这样根据上述路由命名生成与之对应的 URL:

// 打印路由对应的 URL
indexUrl, _ := r.Get("posts.index").URL()
log.Println("文章列表链接:", indexUrl)

createUrl, _ := r.Get("posts.create").URL()
log.Println("发布文章链接:", createUrl)

showUrl, _ := r.Get("posts.show").URL("id", "1")
log.Println("文章详情链接:", showUrl)

打印结果如下:

-w691

gorilla/mux 路由也支持中间件,下篇教程,我们就来介绍如何基于 gorilla/mux 编写并应用路由中间件。

上一篇: 基于 gorilla/mux 包实现路由定义和请求分发:基本使用

下一篇: 基于 gorilla/mux 包实现路由定义和请求分发:路由中间件