1、路由 - http.ServeMux
我们先来看下 Go 标准库 net/http 包里的 http.ServeMux。
ServeMux 和 Handler
Go 语言中处理 HTTP 请求主要跟两个东西相关:ServeMux 和 Handler。
ServeMux 本质上是一个 HTTP 请求路由器(或者叫多路复用器,Multiplexor)。它把收到的请求与一组预先定义的 URL 路径列表做对比,然后在匹配到路径的时候调用关联的处理器(Handler)。
http 的 ServeMux 虽听起来陌生,事实上我们已经在使用它了。
重构:区分不同的 Handler
main.go
package main
import (
"fmt"
"net/http"
)
func defaultHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if r.URL.Path == "/" {
fmt.Fprint(w, "<h1> 你好,欢迎</h1>")
} else {
w.WriteHeader(http.StatusNotFound)
fmt.Fprint(w, "<h1> 页面不存在</h1>")
}
}
func aboutHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprint(w, "<a href = 'https://www.baidu.com'>请联系我们</a>")
}
func main() {
http.HandleFunc("/", defaultHandler)
http.HandleFunc("/about", aboutHandler)
http.ListenAndServe(":8080", nil)
}
http.ServeMux 的局限性
http.ServeMux 在 goblog 中使用,会遇到以下几个问题:
- 不支持 URI 路径参数
- 不支持请求方法过滤
- 不支持路由命名
URI 路径参数
例如说博客详情页,我们的 URI 是 articles/1 这样来查看 ID 为 1 的文章。http.ServeMux 也可以实现,新增一个路由作为示范:
func main() {
router := http.NewServeMux()
router.HandleFunc("/", defaultHandler)
router.HandleFunc("/about", aboutHandler)
// 文章详情
router.HandleFunc("/articles/", func(w http.ResponseWriter, r *http.Request) {
id := strings.SplitN(r.URL.Path, "/", 3)[2]
fmt.Fprint(w, "文章 ID:"+id)
})
http.ListenAndServe(":3000", router)
}
不够直观,且徒增了代码的维护成本。
请求方法过滤
无法直接从路由上区分 POST 或者 GET 等 HTTP 请求方法,只能手动判断,例如:
func main() {
// 列表 or 创建
router.HandleFunc("/articles", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
fmt.Fprint(w, "访问文章列表")
case "POST":
fmt.Fprint(w, "创建新的文章")
}
})
http.ListenAndServe(":3000", router)
}
我们使用 CURL 来测试下:
$ curl http://localhost:3000/articles
访问文章列表%
$ curl -X POST http://localhost:3000/articles
创建新的文章%
可以实现,但是要多出来很多代码。
不支持路由命名
路由命名是一套允许我们快速修改页面里显示 URL 的机制。
例如说我们的文章详情页,URL 是
http://example.com/articles/{id}
项目随着时间的推移,变得非常巨大,在几十个页面里都存在这个 URI 。突然有个需求或者有其他不可控因素,要求我们修改链接为:
http://example.com/posts/{id}
那么我们只能到这个几十个页面里去修改。
使用路由命名的话,我们为 articles/{id} 这个路由命名为 articles.show,几十个页面在编码时,都使用这个路由名称而不是具体的 URI,遇到修改的需求时,我们只需在定义路由这一个地方修改即可。
目前 http.ServeMux 不支持此功能。
http.ServeMux 的优缺点
如上所述,Go 开源社区里有诸多路由器可供选择,那么标准库的 http.ServeMux 对比这些路由器有什么优缺点呢?
优点
- 标准库意味着随着 Go 打包安装,无需另行安装
- 测试充分
- 稳定、兼容性强
- 简单,高效
缺点
- 缺少 Web 开发常见的特性
- 在复杂的项目中使用,需要你写更多的代码
- 开发效率和运行效率,永远是对立面。
Go 因为其诞生的背景(Google 的大流量),以及核心成员的出身(底层语言和系统的缔造者),Go 标准库选择 运行效率 高于 开发效率,所以对一些常见的功能并没有添加到标准库中,这是情有可原的。
然而在 goblog 中,我们将会有数据库等与第三方服务的请求操作,跟这类编码比起来,路由解析这点性能优化微不足道。所以在这个项目中,我们将在性能不会牺牲太大的情况下,选择 开发效率 多一点。
2、集成 Gorilla Mux
为什么不选择 HttpRouter
HttpRouter 是目前来讲速度最快的路由器,且被知名框架 Gin 所采用。
不选择 HttpRouter 的原因是其功能略显单一,没有路由命名功能,不符合我们的要求。
HttpRouter 和 Gin 比较适合在要求高性能,且路由功能要求相对简单的项目中,如 API 或微服务。在全栈的 Web 开发中,gorilla/mux 在性能上虽然有所不及,但是功能强大,比较实用。
安装 gorilla/mux
这是我们第一次安装第三方依赖,goblog 项目将使用官方推荐的 Go Module 来管理第三方依赖。
Go Modules 相关知识下一节再来讲。本节专注于安装和使用 gorilla/mux。
下面初始化 Go Modules:
$ go mod init
接下来使用 go get 命令安装 gorilla/mux :
$ go get -u github.com/gorilla/mux
安装成功后使用 git status 可以看到多出来了两个文件:
On branch master
Untracked files:
(use "git add <file>..." to include in what will be committed)
go.mod
go.sum
nothing added to commit but untracked files present (use "git add" to track)
提示: 下一节,我们再来讲解这两个文件的作用。
使用 gorilla/mux
gorilla/mux 因实现了 net/http 包的 http.Handler接口,故兼容 http.ServeMux ,也就是说,我们可以直接修改一行代码,即可将 gorilla/mux 集成到我们的项目中:
因为 gorilla/mux 的路由解析采用的是 精准匹配 规则,而 net/http 包使用的是 长度优先匹配 规则。
- 精准匹配 指路由只会匹配准确指定的规则,这个比较好理解,也是较常见的匹配方式。
- 长度优先匹配 一般用在静态路由上(不支持动态元素如正则和 URL 路径参数),优先匹配字符数较多的规则。
迁移到 Gorilla Mux
package main
import (
"fmt"
"net/http"
"github.com/gorilla/mux"
)
func homeHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprint(w, "<h1>Hello, 欢迎来到 goblog!</h1>")
}
func aboutHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprint(w, "此博客是用以记录编程笔记,如您有反馈或建议,请联系 "+
"<a href=\"mailto:summer@example.com\">summer@example.com</a>")
}
func notFoundHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusNotFound)
fmt.Fprint(w, "<h1>请求页面未找到 :(</h1><p>如有疑惑,请联系我们。</p>")
}
func articlesShowHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
fmt.Fprint(w, "文章 ID:"+id)
}
func articlesIndexHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "访问文章列表")
}
func articlesStoreHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "创建新的文章")
}
func main() {
router := mux.NewRouter()
router.HandleFunc("/", homeHandler).Methods("GET").Name("home")
router.HandleFunc("/about", aboutHandler).Methods("GET").Name("about")
router.HandleFunc("/articles/{id:[0-9]+}", articlesShowHandler).Methods("GET").Name("articles.show")
router.HandleFunc("/articles", articlesIndexHandler).Methods("GET").Name("articles.index")
router.HandleFunc("/articles", articlesStoreHandler).Methods("POST").Name("articles.store")
// 自定义 404 页面
router.NotFoundHandler = http.HandlerFunc(notFoundHandler)
// 通过命名路由获取 URL 示例
homeURL, _ := router.Get("home").URL()
fmt.Println("homeURL: ", homeURL)
articleURL, _ := router.Get("articles.show").URL("id", "23")
fmt.Println("articleURL: ", articleURL)
http.ListenAndServe(":3000", router)
}
接下来我们一步步分解代码。
1. 新增 homeHandler
首先,因为使用的是精确匹配,我们将 defaultHandler 变更 homeHandler 且将处理 404 的代码移除。
2. 指定 Methods () 来区分请求方法
看下这两个路由:
router.HandleFunc("/articles", articlesIndexHandler).Methods("GET").Name("articles.index")
router.HandleFunc("/articles", articlesStoreHandler).Methods("POST").Name("articles.store")
命令行:
curl http://localhost:3000/articles
访问文章列表%
curl -X POST http://localhost:3000/articles
创建新的文章%
注意: 在 Gorilla Mux 中,如未指定请求方法,默认会匹配所有方法。
3. 请求路径参数和正则匹配
我们的文章详情页面的匹配:
router.HandleFunc("/articles/{id:[0-9]+}", articlesShowHandler).Methods("GET").Name("articles.show")
注意 ID 路径的设置:
{id:[0-9]+}
有以下规则:
- 使用
{name}花括号来设置路径参数 - 在有正则匹配的情况下,使用 : 区分。第一部分是名称,第二部分是正则表达式
[0-9]+
限定了 一个或者多个的数字。如果你访问非数字的 ID ,如 localhost:3000/articles/string 即会看到 404 页面。
再看下在 Handler 里面我们如何获取到这个参数:
func articlesShowHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
fmt.Fprint(w, "文章 ID:"+id)
}
Mux 提供的方法 mux.Vars(r) 会将 URL 路径参数解析为键值对应的 Map,使用以下方法即可读取:
vars["id"]
4. 命名路由与链接生成
看下以下代码:
router.HandleFunc("/", homeHandler).Methods("GET").Name("home")
router.HandleFunc("/articles/{id:[0-9]+}", articlesShowHandler).Methods("GET").Name("articles.show")
Name()方法用来给路由命名,传参是路由的名称,接下来我们就可以靠这个名称来获取到 URI:
homeURL, _ := router.Get("home").URL()
fmt.Println("homeURL: ", homeURL)
articleURL, _ := router.Get("articles.show").URL("id", "1")
fmt.Println("articleURL: ", articleURL)
如果你觉得我的文档能给你带来帮助,请不要吝啬你的赞哦~~