Go 每日一庫之 gorilla-mux
簡介
gorilla/mux
是 gorilla Web 開發工具包中的路由管理庫。gorilla Web 開發包是 Go 語言中輔助開發 Web 服務器的工具包。它包括 Web 服務器開發的各個方面,有表單數據處理包gorilla/schema
,有 websocket 通信包gorilla/websocket
,有各種中間件的包gorilla/handlers
,有 session 管理包gorilla/sessions
,有安全的 cookie 包gorilla/securecookie
。本文先介紹gorilla/mux
(下文簡稱mux
),後續文章會依次介紹上面列舉的 gorilla 包。
mux
有以下優勢:
-
實現了標準的
http.Handler
接口,所以可以與net/http
標準庫結合使用,非常輕量; -
可以根據請求的主機名、路徑、路徑前綴、協議、HTTP 首部、查詢字符串和 HTTP 方法匹配處理器,還可以自定義匹配邏輯;
-
可以在主機名、路徑和請求參數中使用變量,還可以爲之指定一個正則表達式;
-
可以傳入參數給指定的處理器讓其構造出完整的 URL;
-
支持路由分組,方便管理和維護。
快速使用
本文代碼使用 Go Modules。
創建目錄並初始化:
$ mkdir -p gorilla/mux && cd gorilla/mux
$ go mod init github.com/darjun/go-daily-lib/gorilla/mux
安裝gorilla/mux
庫:
$ go get -u github.com/gorilla/gorilla/mux
我現在身邊有幾本 Go 語言的經典著作:
下面我們編寫一個管理圖書信息的 Web 服務。圖書由 ISBN 唯一標識,ISBN 意爲國際標準圖書編號(International Standard Book Number)。
首先定義圖書的結構:
type Book struct {
ISBN string `json:"isbn"`
Name string `json:"name"`
Authors []string `json:"authors"`
Press string `json:"press"`
PublishedAt string `json:"published_at"`
}
var (
mapBooks map[string]*Book
slcBooks []*Book
)
定義init()
函數,從文件中加載數據:
func init() {
mapBooks = make(map[string]*Book)
slcBooks = make([]*Book, 0, 1)
data, err := ioutil.ReadFile("../data/books.json")
if err != nil {
log.Fatalf("failed to read book.json:%v", err)
}
err = json.Unmarshal(data, &slcBooks)
if err != nil {
log.Fatalf("failed to unmarshal books:%v", err)
}
for _, book := range slcBooks {
mapBooks[book.ISBN] = book
}
}
然後是兩個處理函數,分別用於返回整個列表和某一本具體的圖書:
func BooksHandler(w http.ResponseWriter, r *http.Request) {
enc := json.NewEncoder(w)
enc.Encode(slcBooks)
}
func BookHandler(w http.ResponseWriter, r *http.Request) {
book, ok := mapBooks[mux.Vars(r)["isbn"]]
if !ok {
http.NotFound(w, r)
return
}
enc := json.NewEncoder(w)
enc.Encode(book)
}
註冊處理器:
func main() {
r := mux.NewRouter()
r.HandleFunc("/", BooksHandler)
r.HandleFunc("/books/{isbn}", BookHandler)
http.Handle("/", r)
log.Fatal(http.ListenAndServe(":8080", nil))
}
mux
的使用與net/http
非常類似。首先調用mux.NewRouter()
創建一個類型爲*mux.Router
的路由對象,該路由對象註冊處理器的方式與標準庫的*http.ServeMux
完全相同,即調用HandleFunc()
方法註冊類型爲func(http.ResponseWriter, *http.Request)
的處理函數,調用Handle()
方法註冊實現了http.Handler
接口的處理器對象。上面註冊了兩個處理函數,一個是顯示圖書信息列表,一個顯示具體某本書的信息。
注意到路徑/books/{isbn}
使用了變量,在{}
中間指定變量名,它可以匹配路徑中的特定部分。在處理函數中通過mux.Vars(r)
獲取請求r
的路由變量,返回map[string]string
,後續可以用變量名訪問。如上面的BookHandler
中對變量isbn
的訪問。
由於*mux.Router
也實現了http.Handler
接口,所以可以直接將它作爲http.Handle("/", r)
的處理器對象參數註冊。這裏註冊的是根路徑/
,相當於把所有請求的處理都託管給了*mux.Router
。
最後還是http.ListenAndServe(":8080", nil)
開啓一個 Web 服務器,等待接收請求。
運行,在瀏覽器中鍵入localhost:8080
,顯示書籍列表:
鍵入localhost:8080/books/978-7-111-55842-2
,顯示圖書《Go 程序設計語言》的詳細信息:
從上面的使用過程中可以看出,mux
庫非常輕量,能很好的與標準庫net/http
結合使用。
我們還可以使用正則表達式限定變量的模式。ISBN 有固定的模式,現在使用的模式大概是這樣:978-7-111-55842-2
(這就是《Go 程序設計語言》一書的 ISBN),即 3 個數字 - 1 個數字 - 3 個數字 - 5 個數字 - 1 個數字,用正則表達式表示爲\d{3}-\d-\d{3}-\d{5}-\d
。在變量名後添加一個:
分隔變量和正則表達式:
r.HandleFunc("/books/{isbn:\\d{3}-\\d-\\d{3}-\\d{5}-\\d}", BookHandler)
靈活的匹配方式
mux
提供了豐富的匹配請求的方式。相比之下,net/http
只能指定具體的路徑,稍顯笨拙。
我們可以指定路由的域名或子域名:
r.Host("github.io")
r.Host("{subdomain:[a-zA-Z0-9]+}.github.io")
上面的路由只接受域名github.io
或其子域名的請求,例如我的博客地址darjun.github.io
就是它的一個子域名。指定域名時可以使用正則表達式,上面第二行代碼限制子域名的第一部分必須是若干個字母或數字。
指定路徑前綴:
// 只處理路徑前綴爲`/books/`的請求
r.PathPrefix("/books/")
指定請求的方法:
// 只處理 GET/POST 請求
r.Methods("GET", "POST")
使用的協議(HTTP/HTTPS
):
// 只處理 https 的請求
r.Schemes("https")
首部:
// 只處理首部 X-Requested-With 的值爲 XMLHTTPRequest 的請求
r.Headers("X-Requested-With", "XMLHTTPRequest")
查詢參數(即 URL 中?
後的部分):
// 只處理查詢參數包含key=value的請求
r.Queries("key", "value")
最後我們可以組合這些條件:
r.HandleFunc("/", HomeHandler)
.Host("bookstore.com")
.Methods("GET")
.Schemes("http")
除此之外,mux
還允許自定義匹配器。自定義的匹配器就是一個類型爲func(r *http.Request, rm *RouteMatch) bool
的函數,根據請求r
中的信息判斷是否能否匹配成功。http.Request
結構中包含了非常多的信息:HTTP 方法、HTTP 版本號、URL、首部等。例如,如果我們要求只處理 HTTP/1.1 的請求可以這麼寫:
r.MatchrFunc(func(r *http.Request, rm *RouteMatch) bool {
return r.ProtoMajor == 1 && r.ProtoMinor == 1
})
需要注意的是,mux
會根據路由註冊的順序依次匹配。所以,通常是將特殊的路由放在前面,一般的路由放在後面。如果反過來了,特殊的路由就不會被匹配到了:
r.HandleFunc("/specific", specificHandler)
r.PathPrefix("/").Handler(catchAllHandler)
子路由
有時候對路由進行分組管理,能讓程序模塊更清晰,更易於維護。現在網站擴展業務,加入了電影相關信息。我們可以定義兩個子路由分別管理:
r := mux.NewRouter()
bs := r.PathPrefix("/books").Subrouter()
bs.HandleFunc("/", BooksHandler)
bs.HandleFunc("/{isbn}", BookHandler)
ms := r.PathPrefix("/movies").Subrouter()
ms.HandleFunc("/", MoviesHandler)
ms.HandleFunc("/{imdb}", MovieHandler)
子路由一般通過路徑前綴來限定,r.PathPrefix()
會返回一個*mux.Route
對象,調用它的Subrouter()
方法創建一個子路由對象*mux.Router
,然後通過該對象的HandleFunc/Handle
方法註冊處理函數。
電影沒有類似圖書的 ISBN 國際統一標準,只有一個民間 “準標準”:IMDB。我們採用豆瓣電影中的信息:
定義電影的結構:
type Movie struct {
IMDB string `json:"imdb"`
Name string `json:"name"`
PublishedAt string `json:"published_at"`
Duration uint32 `json:"duration"`
Lang string `json:"lang"`
}
加載:
var (
mapMovies map[string]*Movie
slcMovies []*Movie
)
func init() {
mapMovies = make(map[string]*Movie)
slcMovies = make([]*Movie, 0, 1)
data, := ioutil.ReadFile("../../data/movies.json")
json.Unmarshal(data, &slcMovies)
for _, movie := range slcMovies {
mapMovies[movie.IMDB] = movie
}
}
使用子路由的方式,還可以將各個部分的路由分散到各自的模塊去加載,在文件book.go
中定義一個InitBooksRouter()
方法負責註冊圖書相關的路由:
func InitBooksRouter(r *mux.Router) {
bs := r.PathPrefix("/books").Subrouter()
bs.HandleFunc("/", BooksHandler)
bs.HandleFunc("/{isbn}", BookHandler)
}
在文件movie.go
中定義一個InitMoviesRouter()
方法負責註冊電影相關的路由:
func InitMoviesRouter(r *mux.Router) {
ms := r.PathPrefix("/movies").Subrouter()
ms.HandleFunc("/", MoviesHandler)
ms.HandleFunc("/{imdb}", MovieHandler)
}
在main.go
的主函數中:
func main() {
r := mux.NewRouter()
InitBooksRouter(r)
InitMoviesRouter(r)
http.Handle("/", r)
log.Fatal(http.ListenAndServe(":8080", nil))
}
需要注意的是,子路由匹配是需要包含路徑前綴的,也就是說/books/
才能匹配BooksHandler
。
構造路由 URL
我們可以爲一個路由起一個名字,例如:
r.HandleFunc("/books/{isbn}", BookHandler).Name("book")
上面的路由中有參數,我們可以傳入參數值來構造一個完整的路徑:
fmt.Println(r.Get("book").URL("isbn", "978-7-111-55842-2"))
// /books/978-7-111-55842-2 <nil>
返回的是一個*url.URL
對象,其路徑部分爲/books/978-7-111-55842-2
。這同樣適用於主機名和查詢參數:
r := mux.Router()
r.Host("{name}.github.io").
Path("/books/{isbn}").
HandlerFunc(BookHandler).
Name("book")
url, err := r.Get("book").URL("name", "darjun", "isbn", "978-7-111-55842-2")
路徑中所有的參數都需要指定,並且值需要滿足指定的正則表達式(如果有的話)。運行輸出:
$ go run main.go
http://darjun.github.io/books/978-7-111-55842-2
可以調用URLHost()
只生成主機名部分,URLPath()
只生成路徑部分。
中間件
mux
定義了中間件類型MiddlewareFunc
:
type MiddlewareFunc func(http.Handler) http.Handler
所有滿足該類型的函數都可以作爲mux
的中間件使用,通過調用路由對象*mux.Router
的Use()
方法應用中間件。如果看過我上一篇文章《Go 每日一庫之 net/http(基礎和中間件)》應該對這種中間件不陌生了。編寫中間件一般會將原處理器傳入,中間件中會手動調用原處理函數,然後在前後增加通用處理邏輯:
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println(r.RequestURI)
next.ServeHTTP(w, r)
})
}
我在上篇文章中寫的 3 箇中間件可以直接使用,這就是兼容net/http
的好處:
func main() {
logger = log.New(os.Stdout, "[goweb]", log.Lshortfile|log.LstdFlags)
r := mux.NewRouter()
// 直接使用上一篇文章中定義的中間件
r.Use(PanicRecover, WithLogger, Metric)
InitBooksRouter(r)
InitMoviesRouter(r)
http.Handle("/", r)
log.Fatal(http.ListenAndServe(":8080", nil))
}
如果不手動調用原處理函數,那麼原處理函數就不會執行,這可以用來在校驗不通過時直接返回錯誤。例如,網站需要登錄才能訪問,而 HTTP 是一個無狀態的協議。所以發明了 Cookie 機制用於在客戶端和服務器之間記錄一些信息。
我們在登錄成功之後生成一個鍵爲token
的 Cookie 表示已登錄成功,我們可以編寫一箇中間件來出來這塊邏輯,如果 Cookie 不存在或者非法,則重定向到登錄界面:
func login(w http.ResponseWriter, r *http.Request) {
ptTemplate.ExecuteTemplate(w, "login.tpl", nil)
}
func doLogin(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
username := r.Form.Get("username")
password := r.Form.Get("password")
if username != "darjun" || password != "handsome" {
http.Redirect(w, r, "/login", http.StatusFound)
return
}
token := fmt.Sprintf("user, username, password)
data := base64.StdEncoding.EncodeToString([]byte(token))
http.SetCookie(w, &http.Cookie{
Name: "token",
Value: data,
Path: "/",
HttpOnly: true,
Expires: time.Now().Add(24 * time.Hour),
})
http.Redirect(w, r, "/", http.StatusFound)
}
上面爲了記錄登錄狀態,我將登錄的用戶名和密碼組合成username=xxx&password=xxx
形式的字符串,對這個字符串進行base64
編碼,然後設置到 Cookie 中。Cookie 有效期爲 24 小時。同時爲了安全只允許 HTTP 訪問此 Cookie(JS 腳本不可訪問)。當然這種方式安全性很低,這裏只是爲了演示。登錄成功之後重定向到/
。
爲了展示登錄界面,我創建了幾個template
模板文件,使用html/template
解析:
登錄展示頁面:
// login.tpl
<form action="/login" method="post">
<label>Username:</label>
<input ><br>
<label>Password:</label>
<input ><br>
<button type="submit">登錄</button>
</form>
主頁面
<ul>
<li><a href="/books/">圖書</a></li>
<li><a href="/movies/">電影</a></li>
</ul>
同時也創建了圖書和電影的頁面:
// movies.tpl
<ol>
{{ range . }}
<li>
<p>書名: <a href="/movies/{{ .IMDB }}">{{ .Name }}</a></p>
<p>上映日期: {{ .PublishedAt }}</p>
<p>時長: {{ .Duration }}分</p>
<p>語言: {{ .Lang }}</p>
</li>
{{ end }}
</ol>
// movie.tpl
<p>IMDB: {{ .IMDB }}</p>
<p>電影名: {{ .Name }}</p>
<p>上映日期: {{ .PublishedAt }}</p>
<p>時長: {{ .Duration }}分</p>
<p>語言: {{ .Lang }}</p>
圖書頁面類似。接下來要解析模板:
var (
ptTemplate *template.Template
)
func init() {
var err error
ptTemplate, err = template.New("").ParseGlob("./tpls/*.tpl")
if err != nil {
log.Fatalf("load templates failed:%v", err)
}
}
訪問對應的頁面邏輯:
func MoviesHandler(w http.ResponseWriter, r *http.Request) {
ptTemplate.ExecuteTemplate(w, "movies.tpl", slcMovies)
}
func MovieHandler(w http.ResponseWriter, r *http.Request) {
movie, ok := mapMovies[mux.Vars(r)["imdb"]]
if !ok {
http.NotFound(w, r)
return
}
ptTemplate.ExecuteTemplate(w, "movie.tpl", movie)
}
執行對應的模板,傳入電影列表或某個具體的電影信息即可。現在頁面沒有限制訪問,我們來編寫一箇中間件限制只有登錄用戶才能訪問,未登錄用戶訪問時跳轉到登錄界面:
func authenticateMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("token")
if err != nil {
// no cookie
http.Redirect(w, r, "/login", http.StatusFound)
return
}
data, _ := base64.StdEncoding.DecodeString(cookie.Value)
values, _ := url.ParseQuery(string(data))
if values.Get("username") != "dj" && values.Get("password") != "handsome" {
// failed
http.Redirect(w, r, "/login", http.StatusFound)
return
}
next.ServeHTTP(w, r)
})
}
再次強調,這裏只是爲了演示,這種驗證方式安全性很低。
然後,我們讓books
和movies
子路由應用中間件authenticateMiddleware
(需要登錄驗證),而login
子路由不用:
func InitBooksRouter(r *mux.Router) {
bs := r.PathPrefix("/books").Subrouter()
// 這裏
bs.Use(authenticateMiddleware)
bs.HandleFunc("/", BooksHandler)
bs.HandleFunc("/{isbn}", BookHandler)
}
func InitMoviesRouter(r *mux.Router) {
ms := r.PathPrefix("/movies").Subrouter()
// 這裏
ms.Use(authenticateMiddleware)
ms.HandleFunc("/", MoviesHandler)
ms.HandleFunc("/{id}", MovieHandler)
}
func InitLoginRouter(r *mux.Router) {
ls := r.PathPrefix("/login").Subrouter()
ls.Methods("GET").HandlerFunc(login)
ls.Methods("POST").HandlerFunc(doLogin)
}
運行程序(注意多文件程序運行方式):
$ go run .
訪問localhost:8080/movies/
,會重定向到localhost:8080/login
。輸入用戶名darjun
,密碼handsome
,登錄成功顯示主頁面。後面的請求都不需要驗證了,請隨意點擊點擊吧😀
總結
本文介紹了輕量級的,功能強大的路由庫gorilla/mux
。它支持豐富的請求匹配方法,子路由能極大地方便我們管理路由。由於兼容標準庫net/http
,所以可以無縫集成到使用net/http
的程序中,利用爲net/http
編寫的中間件資源。下一篇我們介紹gorilla/handlers
——一些常用的中間件。
大家如果發現好玩、好用的 Go 語言庫,歡迎到 Go 每日一庫 GitHub 上提交 issue😄
參考
-
gorilla/mux GitHub:github.com/gorilla/gorilla/mux
-
Go 每日一庫 GitHub:https://github.com/darjun/go-daily-lib
我
我的博客:https://darjun.github.io
歡迎關注我的微信公衆號【GoUpUp】,共同學習,一起進步~
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/aCj8VjYtcthVOJNfWefFBA