2

Introduction

gorilla/mux is the routing management library in the gorilla web development kit. The gorilla Web development kit is a toolkit for assisting the development of Web servers in the Go language. It includes all aspects of the development of the Web server, there is a form of data processing package gorilla/schema , there websocket communications package gorilla/websocket , there are a variety of middleware packages gorilla/handlers , there is session management pack gorilla/sessions , security of cookie packages gorilla/securecookie . This article first introduces gorilla/mux (hereinafter referred to as mux ), and subsequent articles will introduce the gorilla packages listed above in turn.

mux has the following advantages:

  • Realize the standard http.Handler interface, so it can be net/http standard library, which is very lightweight;
  • The processor can be matched according to the requested host name, path, path prefix, protocol, HTTP header, query string and HTTP method, and the matching logic can be customized;
  • You can use variables in the host name, path, and request parameters, and you can specify a regular expression for it;
  • You can pass in parameters to the specified processor to make it construct a complete URL;
  • Support routing grouping, which is convenient for management and maintenance.

Quick to use

The code in this article uses Go Modules.

Create a directory and initialize:

$ mkdir -p gorilla/mux && cd gorilla/mux
$ go mod init github.com/darjun/go-daily-lib/gorilla/mux

Install the gorilla/mux library:

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

There are several classic books of Go language around me now:

Below we write a Web service for managing book information. Books are uniquely identified by ISBN, which means International Standard Book Number (International Standard Book Number).

First define the structure of the book:

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
)

Define the init() function to load data from the file:

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
  }
}

Then there are two processing functions, which are used to return the entire list and a specific 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)
}

Register the processor:

func main() {
  r := mux.NewRouter()
  r.HandleFunc("/", BooksHandler)
  r.HandleFunc("/books/{isbn}", BookHandler)
  http.Handle("/", r)
  log.Fatal(http.ListenAndServe(":8080", nil))
}

mux use and net/http very similar. First, call mux.NewRouter() create a *mux.Router . The way the routing object registers the processor is exactly the same as that of the *http.ServeMux HandleFunc() method to register the processing function of func(http.ResponseWriter, *http.Request) Handle() method to register and implement the http.Handler interface processing.器Object. Two processing functions are registered above, one is to display the book information list, and the other is to display the information of a specific book.

Note that the path /books/{isbn} uses variables. {} , which can match a specific part of the path. In the handler by mux.Vars(r) acquisition request r routing variables, return map[string]string , subsequent access may be variable name. Such as the access to variable isbn BookHandler

Since *mux.Router also implements the http.Handler interface, it can be directly registered as the processor object parameter of http.Handle("/", r) Registered here is the root path / , which is equivalent to hosting all request processing to *mux.Router .

Finally, http.ListenAndServe(":8080", nil) a Web server and waits to receive requests.

localhost:8080 in the browser to display a list of books:

Type localhost:8080/books/978-7-111-55842-2 to display the detailed information of the book "Go Programming Language":

It can be seen from the above use process that the mux library is very lightweight and can be used in combination net/http

We can also use regular expressions to limit the pattern of variables. ISBN has a fixed pattern, and the current pattern is roughly as follows: 978-7-111-55842-2 (this is the ISBN in the "Go Programming Language" book), that is, 3 numbers-1 number-3 numbers-5 numbers-1 number , Expressed as \d{3}-\d-\d{3}-\d{5}-\d by regular expression. : after the variable name to separate the variable and the regular expression:

r.HandleFunc("/books/{isbn:\\d{3}-\\d-\\d{3}-\\d{5}-\\d}", BookHandler)

Flexible matching method

mux provides a wealth of ways to match requests. In contrast, net/http can only specify a specific path, which is slightly clumsy.

We can specify the domain name or subdomain name of the route:

r.Host("github.io")
r.Host("{subdomain:[a-zA-Z0-9]+}.github.io")

The above routing only accepts github.io or its subdomains. For example, my blog address darjun.github.io is one of its subdomains. Regular expressions can be used when specifying a domain name. The second line of code above restricts the first part of the subdomain name to be a number of letters or numbers.

Specify the path prefix:

// 只处理路径前缀为`/books/`的请求
r.PathPrefix("/books/")

Specify the request method:

// 只处理 GET/POST 请求
r.Methods("GET", "POST")

Protocol used ( HTTP/HTTPS ):

// 只处理 https 的请求
r.Schemes("https")

capital:

// 只处理首部 X-Requested-With 的值为 XMLHTTPRequest 的请求
r.Headers("X-Requested-With", "XMLHTTPRequest")

Query parameters (that is, the part after ?

// 只处理查询参数包含key=value的请求
r.Queries("key", "value")

Finally we can combine these conditions:

r.HandleFunc("/", HomeHandler)
 .Host("bookstore.com")
 .Methods("GET")
 .Schemes("http")

In addition, mux also allows custom matchers. Custom matcher is a type func(r *http.Request, rm *RouteMatch) bool function, according to a request r whether the information to determine whether the matching is successful. http.Request structure contains a lot of information: HTTP method, HTTP version number, URL, header, etc. For example, if we require that only HTTP/1.1 requests are processed, we can write:

r.MatchrFunc(func(r *http.Request, rm *RouteMatch) bool {
  return r.ProtoMajor == 1 && r.ProtoMinor == 1
})

should be noted that mux will be matched according to the order of route registration. Therefore, the special route is usually placed in the front, and the general route is placed in the back . If it is reversed, the special route will not be matched:

r.HandleFunc("/specific", specificHandler)
r.PathPrefix("/").Handler(catchAllHandler)

Sub-route

Sometimes group management of routing can make program modules clearer and easier to maintain. Now the website has expanded its business and added movie-related information. We can define two sub-routes to manage separately:

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)

The sub-route is generally defined by the path prefix. r.PathPrefix() will return a *mux.Route object, call its Subrouter() method to create a sub-route object *mux.Router , and then register the processing function through the object's HandleFunc/Handle

There is no international uniform standard for ISBN similar to books, only one non-governmental "quasi-standard": IMDB. We use the information from Douban Movies:

Define the structure of the movie:

type Movie struct {
  IMDB        string `json:"imdb"`
  Name        string `json:"name"`
  PublishedAt string `json:"published_at"`
  Duration    uint32 `json:"duration"`
  Lang        string `json:"lang"`
}

load:

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
  }
}

Using sub-routes, you can also distribute the routes of each part to their respective modules to load. Define a InitBooksRouter() book.go be responsible for registering book-related routes:

func InitBooksRouter(r *mux.Router) {
  bs := r.PathPrefix("/books").Subrouter()
  bs.HandleFunc("/", BooksHandler)
  bs.HandleFunc("/{isbn}", BookHandler)
}

Define a InitMoviesRouter() method in the file movie.go be responsible for registering movie-related routes:

func InitMoviesRouter(r *mux.Router) {
  ms := r.PathPrefix("/movies").Subrouter()
  ms.HandleFunc("/", MoviesHandler)
  ms.HandleFunc("/{imdb}", MovieHandler)
}

In the main function of main.go

func main() {
  r := mux.NewRouter()
  InitBooksRouter(r)
  InitMoviesRouter(r)

  http.Handle("/", r)
  log.Fatal(http.ListenAndServe(":8080", nil))
}

It should be noted that the sub-route matching needs to include the path prefix, which means that /books/ can match BooksHandler .

Construct routing URL

We can give a route a name, for example:

r.HandleFunc("/books/{isbn}", BookHandler).Name("book")

There are parameters in the above routing, we can pass in parameter values to construct a complete path:

fmt.Println(r.Get("book").URL("isbn", "978-7-111-55842-2"))
// /books/978-7-111-55842-2 <nil>

What is returned is a *url.URL object whose path part is /books/978-7-111-55842-2 . The same applies to hostnames and query parameters:

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")

All parameters in the path need to be specified, and the value needs to satisfy the specified regular expression (if any). Run output:

$ go run main.go
http://darjun.github.io/books/978-7-111-55842-2

You can call URLHost() to generate only the host name part, and URLPath() to generate only the path part.

Middleware

mux defines the middleware type MiddlewareFunc :

type MiddlewareFunc func(http.Handler) http.Handler

All functions that meet this type can be used as mux , and the middleware is applied by calling the Use() method of the *mux.Router If you have read my last article "Go Net/http (Basic and Middleware)" you should be familiar with this kind of middleware. When writing middleware, the original processor is usually passed in, and the original processing function is manually called in the middleware, and then general processing logic is added before and after:

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)
  })
}

The three middlewares I wrote in the last article can be used directly. This is the advantage of 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))
}

If you do not manually call the original processing function, the original processing function will not be executed. This can be used to directly return an error when the verification fails. For example, a website requires a login to access, and HTTP is a stateless protocol. So the Cookie mechanism was invented to record some information between the client and the server.

After we log in successfully, we generate a token to indicate that we have logged in successfully. We can write a middleware to come out this logic. If the cookie does not exist or is illegal, we will redirect to the login interface:

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)
}

To record the login state above, I logged into the user name and password combination username=xxx&password=xxx form of a string, this string base64 encoded and then set to the Cookie. The cookie is valid for 24 hours. At the same time, for security, only HTTP access to this cookie is allowed (JS script is not accessible). Of course, the security of this method is very low, here is just to demonstrate . After successful login, it will be redirected to / .

In order to show the login interface, I created several template template files and parsed them html/template

Log in to the display page:

// 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>

home page

<ul>
  <li><a href="/books/">图书</a></li>
  <li><a href="/movies/">电影</a></li>
</ul>

Also created pages for books and movies:

// 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>

The book pages are similar. Next we need to parse the template:

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)
  }
}

Visit the corresponding page logic:

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)
}

Execute the corresponding template and pass in the movie list or a specific movie information. Now that the page is not restricted to access, let’s write a middleware to restrict access to only logged-in users. When an unlogged-in user accesses, it will jump to the login interface:

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)
  })
}

Again, this is just for demonstration, this authentication method has very low security.

Then, we let the books and movies sub-routing application middleware authenticateMiddleware (login verification is required), and the login sub-routing is not used:

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)
}

Run the program (note how the multi-file program runs):

$ go run .

If you visit localhost:8080/movies/ , you will be redirected to localhost:8080/login . Enter the user name darjun and the password handsome , and the main page will be displayed if the login is successful. The subsequent requests do not need to be verified, please feel free to click and click 😀

to sum up

This article introduces the lightweight and powerful routing library gorilla/mux . It supports a wealth of request matching methods, and sub-routes can greatly facilitate us to manage routes. Because it is compatible with the standard library net/http , it can be seamlessly integrated into the program net/http , using middleware resources written net/http In the next article, we introduce gorilla/handlers -some commonly used middleware.

If you find a fun and useful Go language library, welcome to submit an issue on the Go Daily Library GitHub😄

reference

  1. gorilla/mux GitHub:github.com/gorilla/gorilla/mux
  2. Go daily one library GitHub: https://github.com/darjun/go-daily-lib

I

My blog: https://darjun.github.io

Welcome to follow my WeChat public account [GoUpUp], learn together and make progress together~


darjun
2.9k 声望359 粉丝