简介

​gorilla/mux​​gorilla/schema​​gorilla/websocket​​gorilla/handlers​​gorilla/sessions​​gorilla/securecookie​​gorilla/mux​​mux​
​mux​
​http.Handler​​net/http​

快速使用

本文代码使用 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 语言的经典著作:

Go 每日一库之 gorilla/mux_中间件

下面我们编写一个管理图书信息的 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)​
​localhost:8080​

Go 每日一库之 gorilla/mux_中间件_02

​localhost:8080/books/978-7-111-55842-2​

Go 每日一库之 gorilla/mux_java_03

​mux​​net/http​
​978-7-111-55842-2​​\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")
​?​
// 只处理查询参数包含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​
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。我们采用豆瓣电影中的信息:

Go 每日一库之 gorilla/mux_go_04

定义电影的结构:

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()​
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)
  })
}
​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​
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​​/​
​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)
  })
}

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

​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????

参考


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

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

欢迎关注我的微信公众号【GoUpUp】,共同学习,一起进步~