PHP转GO,全是干货。路由及其中间件(一)

361 阅读7分钟

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)

如果你觉得我的文档能给你带来帮助,请不要吝啬你的赞哦~~