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
函数注册请求处理程序。考虑这对注册,其中 router
是 mux.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
。 - 诸如
ClichesCreate
和ClichesAll
之类的请求处理程序将(指向)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 应用程序突出了该包的主要功能。试用一下该包,您很可能会成为买家。
评论已关闭。