乐趣区

关于golang:Go-每日一库之-gorillamux

简介

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.RouterUse()办法利用中间件。如果看过我上一篇文章《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("username=%s&password=%s", 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 name="username"><br>
  <label>Password:</label>
  <input name="password" type="password"><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)
  })
}

再次强调,这里只是为了演示,这种验证形式安全性很低。

而后,咱们让 booksmovies子路由利用中间件 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😄

参考

  1. gorilla/mux GitHub:github.com/gorilla/gorilla/mux
  2. Go 每日一库 GitHub:https://github.com/darjun/go-daily-lib

我的博客:https://darjun.github.io

欢送关注我的微信公众号【GoUpUp】,独特学习,一起提高~

退出移动版