4.1 路由 http.ServeMux
goblog 需要一款灵活的路由器来搭配 MVC。Go Web 开发有各式各样的路由器可供选择,我们先来看下 Go 标准库 net/http 包里的 http.ServeMux。
ServeMux 和 Handler
Go 语言中处理 HTTP 请求主要跟两个东西相关:ServeMux 和 Handler。
ServeMux 本质上是一个 HTTP 请求路由器(或者叫多路复用器,Multiplexor)。它把收到的请求与一组预先定义的 URL 路径列表做对比,然后在匹配到路径的时候调用关联的处理器(Handler)。
http 的 ServeMux 虽听起来陌生,事实上我们已经在使用它了。
修改代码如下:
package mainimport ("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>Hello, 欢迎来到 goblog!</h1>")} else {w.WriteHeader(http.StatusNotFound)fmt.Fprint(w, "<h1>请求页面未找到 :(</h1>"+"<p>如有疑惑,请联系我们。</p>")}}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 main() {http.HandleFunc("/", defaultHandler)http.HandleFunc("/about", aboutHandler)http.ListenAndServe(":3000", nil)}
浏览器访问以下三个链接,发现与之前一致:
- localhost:3000/
- localhost:3000/about
-
重构:使用自定义的 ServeMux
handler 通常为 nil,此种情况下会使用 DefaultServeMux。接下来,我们可以自定义一个ServeMux:
package mainimport ("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>Hello, 欢迎来到 goblog!</h1>")} else {w.WriteHeader(http.StatusNotFound)fmt.Fprint(w, "<h1>请求页面未找到 :(</h1>"+"<p>如有疑惑,请联系我们。</p>")}}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 main() {router := http.NewServeMux()router.HandleFunc("/", defaultHandler)router.HandleFunc("/about", aboutHandler)http.ListenAndServe(":3000", router)}
浏览器访问以下三个链接,跟之前返回的一样:
- localhost:3000/about
- localhost:3000/no-where
4.2.集成gorilla/mux
安装 gorilla/mux
下面初始化 Go Modules:
接下来使用$ go mod init
go get命令安装 gorilla/mux :$ go get -u github.com/gorilla/mux
使用 gorilla/mux
gorilla/mux 因实现了 net/http 包的http.Handler接口,故兼容 http.ServeMux ,也就是说,我们可以直接修改一行代码,即可将 gorilla/mux 集成到我们的项目中:func main() {router := mux.NewRouter()router.HandleFunc("/", defaultHandler)router.HandleFunc("/about", aboutHandler)http.ListenAndServe(":3000", router)}
注意: 修改以上代码后保存,因为安装了 Go for Visual Studio Code 插件,VSCode 会自动在文件顶部的
import导入 mux 库,我们无需手动添加。
依次以下链接:
- localhost:3000/
- localhost:3000/about
- localhost:3000/articles
- localhost:3000/no-exists
- localhost:3000/articles/2
- localhost:3000/articles/
可以发现:
- 1、2 和 3 可以正常访问。
- 4 无法访问到自定义的 404 页面
- 5 文章详情页无法访问
- 6 可以访问到文章页面,但是 ID 为空
这是因为 gorilla/mux 的路由解析采用的是 精准匹配 规则,而 net/http 包使用的是 长度优先匹配 规则。
- 精准匹配 指路由只会匹配准确指定的规则,是较常见的匹配方式。
- 长度优先匹配 一般用在静态路由上(不支持动态元素如正则和 URL 路径参数),优先匹配字符数较多的规则
以我们的 goblog 为例:
router.HandleFunc("/", defaultHandler)router.HandleFunc("/about", aboutHandler)
使用 长度优先匹配 规则的 http.ServeMux 会把除了 /about 这个匹配的以外的所有 URI 都使用 defaultHandler 来处理。
而使用 精准匹配 的 gorilla/mux 会把以上两个规则精准匹配到两个链接,/ 为首页,/about 为关于,除此之外都是 404 未找到。
知道这个规则后,配合上面几个测试链接的返回结果,会更好理解。
一般 长度优先匹配 规则用在静态内容处理上比较合适,动态内容,例如我们的 goblog 这种动态网站,使用 精准匹配 会比较方便。
迁移到 Gorilla Mux
基于以上规则,接下来改进代码:main.go
package mainimport ("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)}
