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 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>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 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>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 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)
}