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 虽听起来陌生,事实上我们已经在使用它了。
修改代码如下:

  1. package main
  2. import (
  3. "fmt"
  4. "net/http"
  5. )
  6. func defaultHandler(w http.ResponseWriter, r *http.Request) {
  7. w.Header().Set("Content-Type", "text/html; charset=utf-8")
  8. if r.URL.Path == "/" {
  9. fmt.Fprint(w, "<h1>Hello, 欢迎来到 goblog!</h1>")
  10. } else {
  11. w.WriteHeader(http.StatusNotFound)
  12. fmt.Fprint(w, "<h1>请求页面未找到 :(</h1>"+
  13. "<p>如有疑惑,请联系我们。</p>")
  14. }
  15. }
  16. func aboutHandler(w http.ResponseWriter, r *http.Request) {
  17. w.Header().Set("Content-Type", "text/html; charset=utf-8")
  18. fmt.Fprint(w, "此博客是用以记录编程笔记,如您有反馈或建议,请联系 "+
  19. "<a href=\"mailto:summer@example.com\">summer@example.com</a>")
  20. }
  21. func main() {
  22. http.HandleFunc("/", defaultHandler)
  23. http.HandleFunc("/about", aboutHandler)
  24. http.ListenAndServe(":3000", nil)
  25. }

浏览器访问以下三个链接,发现与之前一致:

  • localhost:3000/
  • localhost:3000/about
  • localhost:3000/no-where

    重构:使用自定义的 ServeMux

    handler 通常为 nil,此种情况下会使用 DefaultServeMux。接下来,我们可以自定义一个ServeMux:

    1. package main
    2. import (
    3. "fmt"
    4. "net/http"
    5. )
    6. func defaultHandler(w http.ResponseWriter, r *http.Request) {
    7. w.Header().Set("Content-Type", "text/html; charset=utf-8")
    8. if r.URL.Path == "/" {
    9. fmt.Fprint(w, "<h1>Hello, 欢迎来到 goblog!</h1>")
    10. } else {
    11. w.WriteHeader(http.StatusNotFound)
    12. fmt.Fprint(w, "<h1>请求页面未找到 :(</h1>"+
    13. "<p>如有疑惑,请联系我们。</p>")
    14. }
    15. }
    16. func aboutHandler(w http.ResponseWriter, r *http.Request) {
    17. w.Header().Set("Content-Type", "text/html; charset=utf-8")
    18. fmt.Fprint(w, "此博客是用以记录编程笔记,如您有反馈或建议,请联系 "+
    19. "<a href=\"mailto:summer@example.com\">summer@example.com</a>")
    20. }
    21. func main() {
    22. router := http.NewServeMux()
    23. router.HandleFunc("/", defaultHandler)
    24. router.HandleFunc("/about", aboutHandler)
    25. http.ListenAndServe(":3000", router)
    26. }

    浏览器访问以下三个链接,跟之前返回的一样:

  • localhost:3000/

  • localhost:3000/about
  • localhost:3000/no-where

    4.2.集成gorilla/mux

    安装 gorilla/mux

    下面初始化 Go Modules:
    1. $ go mod init
    接下来使用 go get 命令安装 gorilla/mux :
    1. $ go get -u github.com/gorilla/mux

    使用 gorilla/mux

    gorilla/mux 因实现了 net/http 包的 http.Handler 接口,故兼容 http.ServeMux ,也就是说,我们可以直接修改一行代码,即可将 gorilla/mux 集成到我们的项目中:
    1. func main() {
    2. router := mux.NewRouter()
    3. router.HandleFunc("/", defaultHandler)
    4. router.HandleFunc("/about", aboutHandler)
    5. http.ListenAndServe(":3000", router)
    6. }

    注意: 修改以上代码后保存,因为安装了 Go for Visual Studio Code 插件,VSCode 会自动在文件顶部的 import 导入 mux 库,我们无需手动添加。

依次以下链接:

  1. localhost:3000/
  2. localhost:3000/about
  3. localhost:3000/articles
  4. localhost:3000/no-exists
  5. localhost:3000/articles/2
  6. localhost:3000/articles/

可以发现:

  • 1、2 和 3 可以正常访问。
  • 4 无法访问到自定义的 404 页面
  • 5 文章详情页无法访问
  • 6 可以访问到文章页面,但是 ID 为空

这是因为 gorilla/mux 的路由解析采用的是 精准匹配 规则,而 net/http 包使用的是 长度优先匹配 规则。

  • 精准匹配 指路由只会匹配准确指定的规则,是较常见的匹配方式。
  • 长度优先匹配 一般用在静态路由上(不支持动态元素如正则和 URL 路径参数),优先匹配字符数较多的规则

以我们的 goblog 为例:

  1. router.HandleFunc("/", defaultHandler)
  2. router.HandleFunc("/about", aboutHandler)

使用 长度优先匹配 规则的 http.ServeMux 会把除了 /about 这个匹配的以外的所有 URI 都使用 defaultHandler 来处理。
而使用 精准匹配 的 gorilla/mux 会把以上两个规则精准匹配到两个链接,/ 为首页,/about 为关于,除此之外都是 404 未找到
知道这个规则后,配合上面几个测试链接的返回结果,会更好理解。
一般 长度优先匹配 规则用在静态内容处理上比较合适,动态内容,例如我们的 goblog 这种动态网站,使用 精准匹配 会比较方便。

迁移到 Gorilla Mux

基于以上规则,接下来改进代码:main.go

  1. package main
  2. import (
  3. "fmt"
  4. "net/http"
  5. "github.com/gorilla/mux"
  6. )
  7. func homeHandler(w http.ResponseWriter, r *http.Request) {
  8. w.Header().Set("Content-Type", "text/html; charset=utf-8")
  9. fmt.Fprint(w, "<h1>Hello, 欢迎来到 goblog!</h1>")
  10. }
  11. func aboutHandler(w http.ResponseWriter, r *http.Request) {
  12. w.Header().Set("Content-Type", "text/html; charset=utf-8")
  13. fmt.Fprint(w, "此博客是用以记录编程笔记,如您有反馈或建议,请联系 "+
  14. "<a href=\"mailto:summer@example.com\">summer@example.com</a>")
  15. }
  16. func notFoundHandler(w http.ResponseWriter, r *http.Request) {
  17. w.Header().Set("Content-Type", "text/html; charset=utf-8")
  18. w.WriteHeader(http.StatusNotFound)
  19. fmt.Fprint(w, "<h1>请求页面未找到 :(</h1><p>如有疑惑,请联系我们。</p>")
  20. }
  21. func articlesShowHandler(w http.ResponseWriter, r *http.Request) {
  22. vars := mux.Vars(r)
  23. id := vars["id"]
  24. fmt.Fprint(w, "文章 ID:"+id)
  25. }
  26. func articlesIndexHandler(w http.ResponseWriter, r *http.Request) {
  27. fmt.Fprint(w, "访问文章列表")
  28. }
  29. func articlesStoreHandler(w http.ResponseWriter, r *http.Request) {
  30. fmt.Fprint(w, "创建新的文章")
  31. }
  32. func main() {
  33. router := mux.NewRouter()
  34. router.HandleFunc("/", homeHandler).Methods("GET").Name("home")
  35. router.HandleFunc("/about", aboutHandler).Methods("GET").Name("about")
  36. router.HandleFunc("/articles/{id:[0-9]+}", articlesShowHandler).Methods("GET").Name("articles.show")
  37. router.HandleFunc("/articles", articlesIndexHandler).Methods("GET").Name("articles.index")
  38. router.HandleFunc("/articles", articlesStoreHandler).Methods("POST").Name("articles.store")
  39. // 自定义 404 页面
  40. router.NotFoundHandler = http.HandlerFunc(notFoundHandler)
  41. // 通过命名路由获取 URL 示例
  42. homeURL, _ := router.Get("home").URL()
  43. fmt.Println("homeURL: ", homeURL)
  44. articleURL, _ := router.Get("articles.show").URL("id", "23")
  45. fmt.Println("articleURL: ", articleURL)
  46. http.ListenAndServe(":3000", router)
  47. }