使用 gorilla/mux 进行 HTTP 请求路由和验证

gorilla/mux 包以直接、直观的 API 提供了请求路由、验证和其他服务。
174 位读者喜欢这篇文章。
Person standing in front of a giant computer screen with numbers, data

Opensource.com

Go 网络库包含 http.ServeMux 结构类型,它支持 HTTP 请求多路复用(路由):Web 服务器将对托管资源的 HTTP 请求(URI 如 /sales4today)路由到代码处理程序;处理程序执行适当的逻辑,然后发送 HTTP 响应,通常是 HTML 页面。 这是架构的草图

                 +------------+     +--------+     +---------+
HTTP request---->| web server |---->| router |---->| handler |
                 +------------+     +--------+     +---------+

在调用 ListenAndServe 方法启动 HTTP 服务器时

http.ListenAndServe(":8888", nil) // args: port & router

第二个参数 nil 表示 DefaultServeMux 用于请求路由。

gorilla/mux 包有一个 mux.Router 类型,可以替代 DefaultServeMux 或自定义请求多路复用器。在 ListenAndServe 调用中,mux.Router 实例将替换 nil 作为第二个参数。mux.Router 如此吸引人的地方最好通过代码示例来展示

1. 示例 crud Web 应用程序

crud Web 应用程序(见下文)支持四个 CRUD(创建、读取、更新、删除)操作,它们分别对应于四种 HTTP 请求方法:POST、GET、PUT 和 DELETE。在 crud 应用程序中,托管资源是陈词滥调对列表,每对都是一个陈词滥调和一个冲突的陈词滥调,例如这一对

Out of sight, out of mind. Absence makes the heart grow fonder.

可以添加新的陈词滥调对,并且可以编辑或删除现有的陈词滥调对。

crud Web 应用程序

package main

import (
   "gorilla/mux"
   "net/http"
   "fmt"
   "strconv"
)

const GETALL string = "GETALL"
const GETONE string = "GETONE"
const POST string   = "POST"
const PUT string    = "PUT"
const DELETE string = "DELETE"

type clichePair struct {
   Id      int
   Cliche  string
   Counter string
}

// Message sent to goroutine that accesses the requested resource.
type crudRequest struct {
   verb     string
   cp       *clichePair
   id       int
   cliche   string
   counter  string
   confirm  chan string
}

var clichesList = []*clichePair{}
var masterId = 1
var crudRequests chan *crudRequest

// GET /
// GET /cliches
func ClichesAll(res http.ResponseWriter, req *http.Request) {
   cr := &crudRequest{verb: GETALL, confirm: make(chan string)}
   completeRequest(cr, res, "read all")
}

// GET /cliches/id
func ClichesOne(res http.ResponseWriter, req *http.Request) {
   id := getIdFromRequest(req)
   cr := &crudRequest{verb: GETONE, id: id, confirm: make(chan string)}
   completeRequest(cr, res, "read one")
}

// POST /cliches
func ClichesCreate(res http.ResponseWriter, req *http.Request) {
   cliche, counter := getDataFromRequest(req)
   cp := new(clichePair)
   cp.Cliche = cliche
   cp.Counter = counter
   cr := &crudRequest{verb: POST, cp: cp, confirm: make(chan string)}
   completeRequest(cr, res, "create")
}

// PUT /cliches/id
func ClichesEdit(res http.ResponseWriter, req *http.Request) {
   id := getIdFromRequest(req)
   cliche, counter := getDataFromRequest(req)
   cr := &crudRequest{verb: PUT, id: id, cliche: cliche, counter: counter, confirm: make(chan string)}
   completeRequest(cr, res, "edit")
}

// DELETE /cliches/id
func ClichesDelete(res http.ResponseWriter, req *http.Request) {
   id := getIdFromRequest(req)
   cr := &crudRequest{verb: DELETE, id: id, confirm: make(chan string)}
   completeRequest(cr, res, "delete")
}

func completeRequest(cr *crudRequest, res http.ResponseWriter, logMsg string) {
   crudRequests<-cr
   msg := <-cr.confirm
   res.Write([]byte(msg))
   logIt(logMsg)
}

func main() {
   populateClichesList()

   // From now on, this gorountine alone accesses the clichesList.
   crudRequests = make(chan *crudRequest, 8)
   go func() { // resource manager
      for {
         select {
         case req := <-crudRequests:
         if req.verb == GETALL {
            req.confirm<-readAll()
         } else if req.verb == GETONE {
            req.confirm<-readOne(req.id)
         } else if req.verb == POST {
            req.confirm<-addPair(req.cp)
         } else if req.verb == PUT {
            req.confirm<-editPair(req.id, req.cliche, req.counter)
         } else if req.verb == DELETE {
            req.confirm<-deletePair(req.id)
         }
      }
   }()
   startServer()
}

func startServer() {
   router := mux.NewRouter()

   // Dispatch map for CRUD operations.
   router.HandleFunc("/", ClichesAll).Methods("GET")
   router.HandleFunc("/cliches", ClichesAll).Methods("GET")
   router.HandleFunc("/cliches/{id:[0-9]+}", ClichesOne).Methods("GET")

   router.HandleFunc("/cliches", ClichesCreate).Methods("POST")
   router.HandleFunc("/cliches/{id:[0-9]+}", ClichesEdit).Methods("PUT")
   router.HandleFunc("/cliches/{id:[0-9]+}", ClichesDelete).Methods("DELETE")

   http.Handle("/", router) // enable the router

   // Start the server.
   port := ":8888"
   fmt.Println("\nListening on port " + port)
   http.ListenAndServe(port, router); // mux.Router now in play
}

// Return entire list to requester.
func readAll() string {
   msg := "\n"
   for _, cliche := range clichesList {
      next := strconv.Itoa(cliche.Id) + ": " + cliche.Cliche + "  " + cliche.Counter + "\n"
      msg += next
   }
   return msg
}

// Return specified clichePair to requester.
func readOne(id int) string {
   msg := "\n" + "Bad Id: " + strconv.Itoa(id) + "\n"

   index := findCliche(id)
   if index >= 0 {
      cliche := clichesList[index]
      msg = "\n" + strconv.Itoa(id) + ": " + cliche.Cliche + "  " + cliche.Counter + "\n"
   }
   return msg
}

// Create a new clichePair and add to list
func addPair(cp *clichePair) string {
   cp.Id = masterId
   masterId++
   clichesList = append(clichesList, cp)
   return "\nCreated: " + cp.Cliche + " " + cp.Counter + "\n"
}

// Edit an existing clichePair
func editPair(id int, cliche string, counter string) string {
   msg := "\n" + "Bad Id: " + strconv.Itoa(id) + "\n"
   index := findCliche(id)
   if index >= 0 {
      clichesList[index].Cliche = cliche
      clichesList[index].Counter = counter
      msg = "\nCliche edited: " + cliche + " " + counter + "\n"
   }
   return msg
}

// Delete a clichePair
func deletePair(id int) string {
   idStr := strconv.Itoa(id)
   msg := "\n" + "Bad Id: " + idStr + "\n"
   index := findCliche(id)
   if index >= 0 {
      clichesList = append(clichesList[:index], clichesList[index + 1:]...)
      msg = "\nCliche " + idStr + " deleted\n"
   }
   return msg
}

//*** utility functions
func findCliche(id int) int {
   for i := 0; i < len(clichesList); i++ {
      if id == clichesList[i].Id {
         return i;
      }
   }
   return -1 // not found
}

func getIdFromRequest(req *http.Request) int {
   vars := mux.Vars(req)
   id, _ := strconv.Atoi(vars["id"])
   return id
}

func getDataFromRequest(req *http.Request) (string, string) {
   // Extract the user-provided data for the new clichePair
   req.ParseForm()
   form := req.Form
   cliche := form["cliche"][0]    // 1st and only member of a list
   counter := form["counter"][0]  // ditto
   return cliche, counter
}

func logIt(msg string) {
   fmt.Println(msg)
}

func populateClichesList() {
   var cliches = []string {
      "Out of sight, out of mind.",
      "A penny saved is a penny earned.",
      "He who hesitates is lost.",
   }
   var counterCliches = []string {
      "Absence makes the heart grow fonder.",
      "Penny-wise and dollar-foolish.",
      "Look before you leap.",
   }

   for i := 0; i < len(cliches); i++ {
      cp := new(clichePair)
      cp.Id = masterId
      masterId++
      cp.Cliche = cliches[i]
      cp.Counter = counterCliches[i]
      clichesList = append(clichesList, cp)
   }
}

为了专注于请求路由和验证,crud 应用程序不使用 HTML 页面作为对请求的响应。相反,请求会产生纯文本响应消息:陈词滥调对列表是对 GET 请求的响应,确认已将新的陈词滥调对添加到列表是对 POST 请求的响应,等等。这种简化使得使用命令行实用程序(例如 curl)轻松测试应用程序,特别是 gorilla/mux 组件。

可以从 GitHub 安装 gorilla/mux 包。crud 应用程序无限期运行;因此,应使用 Control-C 或等效命令终止它。crud 应用程序的代码以及 README 和示例 curl 测试可在 我的网站上找到。

2. 请求路由

mux.Router 扩展了 REST 风格的路由,它对 HTTP 方法(例如,GET)和 URL 末尾的 URI 或路径(例如,/cliches)给予同等的权重。URI 充当 HTTP 谓词(方法)的名词。例如,在 HTTP 请求中,起始行如

GET /cliches

表示获取所有陈词滥调对,而起始行如

POST /cliches

表示从 HTTP 正文中的数据创建陈词滥调对

crud Web 应用程序中,有五个函数充当五个 HTTP 请求变体的请求处理程序

ClichesAll(...)    # GET: get all of the cliche pairs
ClichesOne(...)    # GET: get a specified cliche pair
ClichesCreate(...) # POST: create a new cliche pair
ClichesEdit(...)   # PUT: edit an existing cliche pair
ClichesDelete(...) # DELETE: delete a specified cliche pair

每个函数接受两个参数:用于将响应发送回请求者的 http.ResponseWriter,以及指向 http.Request 的指针,后者封装了来自底层 HTTP 请求的信息。gorilla/mux 包使注册这些请求处理程序到 Web 服务器以及执行基于正则表达式的验证变得容易。

crud 应用程序中的 startServer 函数注册请求处理程序。考虑这对注册,其中 routermux.Router 实例

router.HandleFunc("/", ClichesAll).Methods("GET")
router.HandleFunc("/cliches", ClichesAll).Methods("GET")

这些语句意味着对单个斜杠 //cliches 的 GET 请求应路由到 ClichesAll 函数,然后由该函数处理请求。例如,curl 请求(以 % 作为命令行提示符)

% curl --request GET localhost:8888/

产生此响应

1: Out of sight, out of mind.  Absence makes the heart grow fonder.
2: A penny saved is a penny earned.  Penny-wise and dollar-foolish.
3: He who hesitates is lost.  Look before you leap.

这三对陈词滥调对是 crud 应用程序中的初始数据。

在这对注册语句中

router.HandleFunc("/cliches", ClichesAll).Methods("GET")
router.HandleFunc("/cliches", ClichesCreate).Methods("POST")

URI 是相同的 (/cliches),但谓词不同:第一个是 GET,第二个是 POST。此注册例证了 REST 风格的路由,因为仅谓词的差异就足以将请求分派到两个不同的处理程序。

虽然在注册中允许使用多个 HTTP 方法,但这有悖于 REST 风格路由的精神

router.HandleFunc("/cliches", DoItAll).Methods("POST", "GET")

HTTP 请求可以基于谓词和 URI 之外的特征进行路由。例如,注册

router.HandleFunc("/cliches", ClichesCreate).Schemes("https").Methods("POST")

需要 HTTPS 访问才能进行 POST 请求以创建新的陈词滥调对。以类似的方式,注册可能需要请求具有指定的 HTTP 标头元素(例如,身份验证凭据)。

3. 请求验证

gorilla/mux 包通过正则表达式采用了一种简单直观的方法来进行请求验证。考虑此用于获取一个操作的请求处理程序

router.HandleFunc("/cliches/{id:[0-9]+}", ClichesOne).Methods("GET")

此注册排除了诸如

% curl --request GET localhost:8888/cliches/foo

之类的 HTTP 请求,因为 foo 不是十进制数字。请求会导致熟悉的 404(未找到)状态代码。在此处理程序注册中包含正则表达式模式可确保仅当请求 URI 以十进制整数值结尾时才调用 ClichesOne 函数来处理请求

% curl --request GET localhost:8888/cliches/3  # ok

作为第二个示例,考虑请求

% curl --request PUT --data "..." localhost:8888/cliches

此请求导致状态代码 405(方法错误),因为在 crud 应用程序中,/cliches URI 仅为 GET 和 POST 请求注册。PUT 请求(如 GET one 请求)必须在 URI 的末尾包含数字 ID

router.HandleFunc("/cliches/{id:[0-9]+}", ClichesEdit).Methods("PUT")

4. 并发问题

gorilla/mux 路由器将对已注册请求处理程序的每次调用都作为单独的 goroutine 执行,这意味着并发性已内置到包中。例如,如果有十个同时发生的请求,例如

% curl --request POST --data "..." localhost:8888/cliches

然后 mux.Router 启动十个 goroutine 来执行 ClichesCreate 处理程序。

在 GET all、GET one、POST、PUT 和 DELETE 这五个请求操作中,最后三个操作会更改请求的资源,即容纳陈词滥调对的共享 clichesList。因此,crud 应用程序需要通过协调对 clichesList 的访问来保证安全并发。用不同但等效的术语来说,crud 应用程序必须防止 clichesList 上的竞争条件。在生产环境中,可以使用数据库系统来存储诸如 clichesList 之类的资源,然后可以通过数据库事务来管理安全并发。

crud 应用程序采用推荐的 Go 方法来实现安全并发

  • 一旦 Web 服务器开始侦听请求,就只有一个 goroutine(在 crud 应用程序 startServer 函数中启动的资源管理器)可以访问 clichesList
  • 诸如 ClichesCreateClichesAll 之类的请求处理程序将(指向)crudRequest 实例发送到 Go 通道(默认情况下是线程安全的),并且只有资源管理器从此通道读取。然后,资源管理器对 clichesList 执行请求的操作。

安全并发架构可以草绘如下

                 crudRequest                   read/write
request handlers------------->resource manager------------>clichesList

使用此架构,无需显式锁定 clichesList,因为一旦 CRUD 请求开始进入,只有一个 goroutine(资源管理器)访问 clichesList

为了使 crud 应用程序尽可能并发,必须在请求处理程序(一方)和单个资源管理器(另一方)之间进行有效的分工。在这里,为了回顾,这是 ClichesCreate 请求处理程序

func ClichesCreate(res http.ResponseWriter, req *http.Request) {
   cliche, counter := getDataFromRequest(req)
   cp := new(clichePair)
   cp.Cliche = cliche
   cp.Counter = counter
   cr := &crudRequest{verb: POST, cp: cp, confirm: make(chan string)}
   completeRequest(cr, res, "create")
}

请求处理程序 ClichesCreate 调用实用程序函数 getDataFromRequest,该函数从 POST 请求中提取新的陈词滥调和反陈词滥调。然后,ClichesCreate 函数创建一个新的 ClichePair,设置两个字段,并创建一个要发送到单个资源管理器的 crudRequest。此请求包括一个确认通道,资源管理器使用该通道将信息返回给请求处理程序。所有设置工作都可以在不涉及资源管理器的情况下完成,因为尚未访问 clichesList

ClichesCreate 函数和其他请求处理程序末尾调用的 completeRequest 实用程序函数

completeRequest(cr, res, "create") // shown above

通过将 crudRequest 放入 crudRequests 通道中,使资源管理器开始工作

func completeRequest(cr *crudRequest, res http.ResponseWriter, logMsg string) {
   crudRequests<-cr          // send request to resource manager
   msg := <-cr.confirm       // await confirmation string
   res.Write([]byte(msg))    // send confirmation back to requester
   logIt(logMsg)             // print to the standard output
}

对于 POST 请求,资源管理器调用实用程序函数 addPair,该函数更改 clichesList 资源

func addPair(cp *clichePair) string {
   cp.Id = masterId  // assign a unique ID
   masterId++        // update the ID counter
   clichesList = append(clichesList, cp) // update the list
   return "\nCreated: " + cp.Cliche + " " + cp.Counter + "\n"
}

资源管理器为其他 CRUD 操作调用类似的实用程序函数。值得重申的是,一旦 Web 服务器开始接受请求,资源管理器是唯一读取或写入 clichesList 的 goroutine。

对于任何类型的 Web 应用程序,gorilla/mux 包都以直接、直观的 API 提供了请求路由、请求验证和相关服务。crud Web 应用程序突出了该包的主要功能。试用一下该包,您很可能会成为买家。

标签
User profile image.
我是计算机科学领域的学者(德保罗大学计算与数字媒体学院),在软件开发方面拥有广泛的经验,主要是在生产计划和调度(钢铁行业)以及产品配置(卡车和公共汽车制造)方面。有关书籍和其他出版物的详细信息,请访问

评论已关闭。

Creative Commons License本作品根据 Creative Commons Attribution-Share Alike 4.0 International License 获得许可。
© . All rights reserved.