Golang使用gorilla、mux包

警告
本文最后更新于 2020-05-29,文中内容可能已过时。

本文介绍 gorilla/mux 包的使用。

我们已知 Go 标准库 net/http 提供的默认路由是 DefaultServeMux,虽然简单易上手,但存在很多不足,比如

  • 不支持参数设定,例如 /user/:uid 这种泛类型匹配;
  • 对 REST 风格接口支持不友好,无法限制访问路由的方法;
  • 对于拥有很多路由规则的应用,编写大量路由规则非常繁琐。

为此,我们可以使用第三方库 gorilla/mux 提供的更加强大的路由处理器(mux 代表 HTTP request multiplexer,即 HTTP 请求多路复用器),和 http.ServeMux 实现原理一样,gorilla/mux 提供的路由器实现类 mux.Router 也会匹配用户请求与系统注册的路由规则,然后将用户请求转发过去。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
type Router struct {
	// Configurable Handler to be used when no route matches.
	NotFoundHandler http.Handler
	// Configurable Handler to be used when the request method does not match the route.
	MethodNotAllowedHandler http.Handler
	// Routes to be matched, in order.
	routes []*Route
	// Routes by name for URL building.
	namedRoutes map[string]*Route
	// If true, do not clear the request context after handling the request.
	// Deprecated: No effect, since the context is stored on the request itself.
	KeepContext bool
	// Slice of middlewares to be called after a match is found
	middlewares []middleware
	// configuration shared with `Route`
	routeConf
}

mux.Router 主要具备以下特性:

  • 实现了 http.Handler 接口,所以和 http.ServeMux 完全兼容;
  • 可以基于 URL 主机、路径、前缀、scheme、请求头、请求参数、请求方法进行路由匹配;
  • URL 主机、路径、查询字符串支持可选的正则匹配;
  • 支持构建或反转已注册的 URL 主机,以便维护对资源的引用;
  • 支持路由嵌套,以便不同路由可以共享通用条件,比如主机、路径前缀等。

1. 使用入门

运行如下命令进行安装

1
$ go get -u github.com/gorilla/mux

一个简单的示例如下

1
2
3
4
5
6
7
func main() {
    r := mux.NewRouter()
    r.HandleFunc("/", HomeHandler)
    r.HandleFunc("/products", ProductsHandler)
    r.HandleFunc("/articles", ArticlesHandler)
    http.ListenAndServe(":8080", r)
}

main 函数中的第一行显式初始化了 mux.Router 作为路由器,然后在这个路由器中注册路由规则,最后将这个路由器传入 http.ListenAndServe 方法,整个调用过程和之前并无二致,因为mux.Router 也实现了 Handler 接口。

路径中可以包含变量。变量的定义形式为 {name}{name:pattern},只能是小写字母,不支持其它字符,同时,name 可以是正则表达式,如下面的例子所示

1
2
3
4
r := mux.NewRouter()
r.HandleFunc("/products/{key}", ProductHandler)
r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler)
r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler)

相应地,在闭包处理函数中,我们使用 mux.Vars() 解析路由参数:

1
2
3
4
5
func ArticlesCategoryHandler(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    w.WriteHeader(http.StatusOK)
    fmt.Fprintf(w, "Category: %v\n", vars["category"])
}

2. 路由代码拆分

比较简单的情况下,所有的路由、处理器都放在应用入口文件中,一般是 main.go,但如果项目比较大,甚至仅仅是博客这种级别的项目,就要处理文章、用户、图片等众多资源,所以我们需要针对这种情况进行一定的优化。

优化的办法就是将路由器与控制器分离,为了使代码结构更加清晰明了,我们把服务器、路由器、路由定义、处理器方法全都拆分开。

2.1 路由器

我们在项目根目录下新建 routes 目录用来存放路由定义及实现。

首先在 routes 目录下创建 routes.go 存放路由定义,文件内容如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package routes

import "net/http"

// 定义一个 WebRoute 结构体用于存放单个路由
type WebRoute struct {
    Name        string
    Method      string
    Pattern     string
    HandlerFunc http.HandlerFunc
}

// 声明 WebRoutes 切片存放所有 Web 路由
type WebRoutes []WebRoute

// 定义所有 Web 路由
var webRoutes = WebRoutes{
    
}

在这里,我们定义了一个 WebRoute 结构体来表示单个路由,其中包含了路由名称、请求方法、匹配字符串模式、以及对应的处理器方法,路由器可以根据这些配置请求请求分发。

然后定义了一个 WebRoutes 切片来存放所有 WebRoute 类型的路由,最后初始化这个切片为空,表示还没有定义任何路由。

接下来,在 routes 目录下创建 router.go 用来编写路由器实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package routes

import "github.com/gorilla/mux"

// 返回一个 mux.Router 类型指针,从而可以当作处理器使用
func NewRouter() *mux.Router {

    // 创建 mux.Router 路由器示例
    router := mux.NewRouter().StrictSlash(true)

    // 遍历 web.go 中定义的所有 webRoutes
    for _, route := range webRoutes {
        // 将每个 web 路由应用到路由器
        router.Methods(route.Method).
            Path(route.Pattern).
            Name(route.Name).
            Handler(route.HandlerFunc)
    }

    return router
}

StrictSlash 定义斜杠尾随行为,意思是,传入 true 时,如果路由路径是 /path 这种形式,将重定向到 /path/ ,反之亦然。传入 false 时,不会重定向,这两种情况不会看作一种。

Methods,Path,Name,Handler分别用来限定请求方法、匹配字符串模式、路由名和处理器方法。通过这种方式,我们将 routes.go 中定义的所有 Web 路由都应用到了使用 mux.NewRouter 创建的路由器,以便可以处理用户请求的路由匹配和分发。

2.2 服务器

在入口文件 main.go 中使用如下方法启动服务器

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
     . "github.com/shuzang/projectname/routes"
    "log"
    "net/http"
)

func main()  {
    startWebServer("8080")
}

func startWebServer(port string)  {
    r := NewRouter()
    http.Handle("/", r)

    log.Println("Starting HTTP service at " + port)
    err := http.ListenAndServe(":"+port, nil) // Goroutine will block here

    if err != nil {
        log.Println("An error occured starting HTTP listener at port " + port)
        log.Println("Error: " + err.Error())
    }
}

我们将 Web 服务器启动逻辑封装到 startWebServer 方法中实现,该方法需要传入端口参数。在具体实现时,我们调用了 routes/router.go 中定义的 NewRouter 方法,将其返回值作为处理器传入 http.Handle 方法,最后调用 http.ListenAndServe 启动 Web 服务器并监听传入的端口号。

最后在 main 方法中调用 startWebServer 方法即可。

2.3 处理器

上层代码写完后,现在定义处理器方法。在项目根目录下新建 handlers 目录存放处理器方法,这里举 2 个示例,分别定义在 common.gouser.go 两个文件中,用来处理通用请求和用户资源。

首先在 common.go 中编写首页请求处理器方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package handlers

import (
    "io"
    "net/http"
)

func Home(w http.ResponseWriter, r *http.Request)  {
    io.WriteString(w, "Welcome to my site")
}

然后在 user.go 中定义获取指定用户对应处理器方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package handlers

import (
    "github.com/gorilla/mux"
    "io"
    "net/http"
)

func GetUser(w http.ResponseWriter, r *http.Request)  {
    // Get user from DB by id...
    params := mux.Vars(r)
    id := params["id"]
    io.WriteString(w, "Return user info with id = " + id)
}

这时要记得,routes/routes.go 中的路由切片还是空的,用实现的处理器填充它

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
var webRoutes = WebRoutes{
    WebRoute{
        "Home",
        "GET",
        "/",
        handlers.Home,
    },
    WebRoute{
        "User",
        "GET",
        "/user/{id}",
        handlers.GetUser,
    },
}

3. 路由匹配规则

第一部分的路由匹配规则只是简单介绍,实际上,gorilla/mux 实现的匹配规则非常强大。

3.1 常用匹配规则

限定请求方法

1
2
3
4
r.HandleFunc("/books/{title}", CreateBook).Methods("POST")
r.HandleFunc("/books/{title}", ReadBook).Methods("GET")
r.HandleFunc("/books/{title}", UpdateBook).Methods("PUT")
r.HandleFunc("/books/{title}", DeleteBook).Methods("DELETE")

限定主机名或子域名

1
r.HandleFunc("/books/{title}", BookHandler).Host("www.mybookstore.com")

限定 Scheme

1
2
r.HandleFunc("/secure", SecureHandler).Schemes("https")
r.HandleFunc("/insecure", InsecureHandler).Schemes("http")

限定前缀和子路由

1
2
3
bookrouter := r.PathPrefix("/books").Subrouter()
bookrouter.HandleFunc("/", AllBooks)
bookrouter.HandleFunc("/{title}", GetBook)

限定请求参数

1
2
3
4
r.HandleFunc("/request/header", func(w http.ResponseWriter, r *http.Request) {
    header := "X-Requested-With"
    fmt.Fprintf(w, "包含指定请求头[%s=%s]", header, r.Header[header])
}).Headers("X-Requested-With", "XMLHttpRequest")

3.2 自定义匹配规则

gorilla/mux 路由支持通过 MatcherFunc 方法自定义路由匹配规则,在该方法中,可以获取到请求实例 request,这样我们就可以拿到所有的用户请求信息,并对其进行判断,符合我们预期的请求才能匹配并访问该方法应用到的路由。

比如下面这个示例,我们限定只有来自 https://baidu.com 域名的请求才可以匹配到 /custom/matcher 路由

1
2
3
4
5
r.HandleFunc("/custom/matcher", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "请求来自指定域名: %s", r.Referer())
}).MatcherFunc(func(request *http.Request, match *mux.RouteMatch) bool {
    return request.Referer() == "https://baidu.com"
})

3.3 路由命名

通过 Name 方法在路由规则中指定

1
2
3
postRouter := r.PathPrefix("/posts").Subrouter()
postRouter.HandleFunc("/", listPosts).Methods("GET").Name("posts.index")
postRouter.HandleFunc("/create", createPost).Methods("POST").Name("posts.create")

4. 路由中间件

5. 处理静态资源响应

使用默认http包处理静态资源的方法如下

1
2
fs := http.FileServer(http.Dir("assets/"))
http.Handle("/static/", http.StripPrefix("/static/", fs))

使用 gorilla/mux 时,处理方法很相似

1
2
3
4
5
6
7
8
r := NewRouter() // 通过 router.go 中定义的路由器来分发请求

assets := http.FileServer(http.Dir("public"))
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", assets))

http.Handle("/", r) // 应用路由器到 HTTP 服务器

...

虽然 gorilla/mux 路由器提供了对静态资源的支持,但是通常我们还是会基于 Nginx 来处理静态资源,然后将动态请求转发给 Go HTTP 服务器,因为 Nginx 作为一款强大的反向代理服务器,并发处理静态资源的能力非常强悍,没必要自己去处理这块逻辑。

支付宝
微信
0%