Go 的net
包方便了 Go 程序中的所有网络通信,无论是通过 HTTP、TCP/IP、WebSockets 还是任何其他标准网络协议。
当然,因为这本书是针对网络开发的,所以我们主要关注的是 HTTP,这意味着我们将使用的主要子包是net/http
。
在本章中,我们将了解任何 Go web 应用程序的基本需求——服务和路由——以及如何使用net/http
和补充包来实现它们。
回到过去,web 服务器只不过是提供驻留在服务器目录中的文件。如果这就是我们现在想要的,我们可以在几行代码中使用net/http package
在 Go 中做一些非常相似的事情。
代码清单 1:作为网络服务器运行
package
main
import
(
"net/http"
)
func
main() {
http.ListenAndServe(":8999",
http.FileServer(http.Dir("/var/www")))
}
这是非常基本的,但是我们的程序履行了任何 web 服务器的核心职责——即,倾听请求和提供响应。更重要的是,因为它不必担心所有其他问题,一个更传统的网络服务器必须,程序是闪电般的速度。
在这个例子中,我们调用http.ListenAndServe()
函数将端口 8999 上的所有请求发送到http.FileServer
处理程序方法,该方法依次接受服务器上我们想要提供文件的目录。放轻松!
如果您使用 go run <*program_name.go*>
运行程序,并在浏览器的地址栏中输入http://localhost:8999/
,后跟服务器上存在于/var/www
中的一个文件的名称,您将看到该文件的内容被显示出来,如图 4 所示。
图 4:去服务地鼠
然而,大多数现代 web 应用程序对服务器的要求比这多一点。请记住,您不会总是发送静态内容。您将越来越多地被要求动态地生成内容,也许是从数据库的内容中。在这种情况下,物理文件位置没有多大意义。
假设您有一个更复杂的站点结构,其中应用程序的文件分布在不同的子目录中?比如www.mysite.com/about/aboutus.html
和www.mysite.com/blog/blog.html
?这种方法行不通。因此,您需要更好地控制应用程序可以接受的网址。您可以通过使用net/http
包的路由功能来实现这一点。
Go 依赖两个主要组件来处理 HTTP 请求——多路复用器和处理程序。多路复用器(或“多路复用器”)本质上是一个 HTTP 请求路由器。在围棋的net/http
包中,多路复用器功能由ServeMux
提供,默认的发球多路复用器是DefaultServeMux
。
直觉,嗯?
ServeMux
将传入的请求与预定义的网址路径列表进行比较,然后在匹配时为每个路径调用适当的处理程序(您定义的函数)。
让我们首先看一下代码,然后我们将检查发生了什么。
代码清单 2:使用网络/http 的基本服务和路由
package
main
import
(
"fmt"
"html"
"log"
"net/http"
"time"
)
func
main() {
http.HandleFunc("/", showInfo)
http.HandleFunc("/site", serveFile)
err
:= http.ListenAndServe(":8999", nil)
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}
func
showInfo(w http.ResponseWriter, r *http.Request)
{
fmt.Fprintln(w,
"Current time: ", time.Now())
fmt.Fprintln(w,
"URL Path: ",
html.EscapeString(r.URL.Path))
}
func
serveFile(w http.ResponseWriter, r *http.Request)
{
http.ServeFile(w,
r, "index.html")
}
请注意,main()
函数是程序的入口点,它通过使用http.HandleFunc(*route, handler*)
方法将网址路由映射到一个函数来设置一些服务器路由,该函数将响应任何与该路由匹配的请求。
程序接下来调用http.ListenAndServe()
方法,该方法用指定的地址(在本例中是端口 8999 上的本地机器)和多路复用器启动一个 HTTP 服务器。这种情况下的多路复用器是nil
,它告诉 Go 使用DefaultServeMux
。
根据访问的网址是在网站的根目录(“/
”)还是在“/site
”,处理函数会做不同的事情。然而,在这两种情况下,它们的方法签名必须同时实现http.ResponseWriter
和http.Request
。如果一个没有实现这两者的处理程序通过http.HandleFunc()
被调用,你将会看到 Go 将会引发一个编译时错误。
如果用户访问/site
,则http.ServeFile()
方法处理该请求并返回与应用程序位于同一目录的index.html
页面。您可以使用http.ServeFile()
发送响应中的任何静态文件。
如果用户访问根位置,将调用showInfo()
,并显示当前时间以及用户在根网址之外输入的任何路径。应用程序通过使用传递给处理程序的http.Request
对象的URL.Path
属性来提取这些信息。响应由http.ResponseWriter
生成。
通过从您的 IDE 发出go run <*program_name*.*go*>
或简单地通过命令行启动程序。接下来,在浏览器中访问localhost:8999
。尝试各种组合来测试它的功能。
例如,输入http://localhost:8999
会产生图 5 中动态生成的页面。
图 5:访问网络应用程序的根目录
输入任何子路线(例如,localhost:8999/hello/there/from/golang
)会显示输入的时间和日期以及子路线,如图 6 所示。
图 6:查看子路径
进入localhost:8999/site
显示index.html
页面。
图 7:访问/站点
在这个例子中,让我们扩展服务静态文件的能力。假设请求中的 URL 以/static/
开头,我们想要剥离 URL 的/static
部分,以便在/var/www
目录的剩余路径中查找引用的文件。我们可以使用StripPrefix
函数来实现这一点,如代码清单 3 所示。
代码清单 3:使用 StripPrefix
package
main
import
(
"fmt"
"html"
"log"
"net/http"
"time"
)
func
main() {
http.HandleFunc("/", showInfo)
files
:= http.FileServer(http.Dir("/var/www"))
http.Handle("/site/", http.StripPrefix("/site/", files))
err
:= http.ListenAndServe(":8999", nil)
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}
func
showInfo(w http.ResponseWriter, r *http.Request)
{
fmt.Fprintln(w,
"Current time: ", time.Now())
fmt.Fprintln(w,
"URL Path: ",
html.EscapeString(r.URL.Path))
}
请注意,在本例中,我们并未调用HandleFunc
,而是调用Handle
。这是因为FileServer
函数返回它自己的处理程序,我们可以使用Handle
将它传递给 mux(而不是显式创建我们自己的处理程序函数)。
因此,您可以看到,让一个自助式网站(不需要单独的 web 服务器)开始运行并让您响应一些简单的请求是很简单的。
在 web 开发中,中间件是位于 web 请求和路由处理器之间的代码。中间件由可重用的代码组成,您可以使用这些代码来执行必须在调用处理程序之前或之后发生的任务。
“中间件”一词经常用于 Go 编程,但您也可能会看到其他 web 语言和技术使用的类似术语,例如“拦截器”、“挂钩”和“请求过滤”
例如,您可能希望在路由请求之前检查数据库连接的状态或对用户进行身份验证。您可能还想压缩响应的内容或限制调用特定处理程序的次数,这可能是您对免费访问您的 web 服务的用户施加的一些限制的一部分。
创建中间件只是链接处理程序和处理程序函数的问题,这是您将在 Go web 开发中经常看到和使用的东西。
基本思想是将一个处理函数——我们称之为F2——作为参数传递给另一个处理函数——我们称之为这个 f1 。处理程序 f1 在触发它的路线被访问时被调用。 f1 做一些工作,然后调用 f2 。
当然,你可以让 f1 直接呼叫 f2 。然而,这并不理想,因为我们通常希望实现明确的关注点分离,因此我们的处理程序代码应该真正限于处理请求,而不是做 f2 设计要做的任何事情。
这里有一个基本的想法——不是把响应作为处理程序的一部分来处理,而是简单地把链中的下一个处理程序传递给它。
接受另一个处理函数作为参数并返回新函数的函数可以在调用处理函数之前或之后执行任务(或者前后都执行),甚至可以最终选择完全不调用原始处理函数(如果这是您的意图)。
考虑代码清单 4 中的例子。
代码清单 4:中间件示例 1
package
main
import
(
"fmt"
"net/http"
)
func
middleware1(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w,
"Executing middleware1()...")
next.ServeHTTP(w,
r)
fmt.Fprintln(w,
"Executing middleware1() again...")
})
}
func
middleware2(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w,
"Executing middleware2()...")
if r.URL.Path !=
"/" {
return
}
next.ServeHTTP(w,
r)
fmt.Fprintln(w,
"Executing middleware2() again...")
})
}
func
final(w http.ResponseWriter, r *http.Request)
{
fmt.Fprintln(w, "Executing final()...")
fmt.Fprintln(w,
"Done")
}
func
main() {
finalHandler
:= http.HandlerFunc(final)
http.Handle("/",
middleware1(middleware2(finalHandler)))
http.ListenAndServe(":8999", nil)
}
看main()
功能。这里,我们用middleware1
处理程序拦截对我们的根网络目录的请求。该处理程序接受另一个名为middleware2
的处理程序作为参数。这又将final
处理程序作为参数。
当有人访问我们的网络应用程序时,它会调用middleware1
,显示一条消息,当它点击next.serveHTTP
时,在middleware2
执行代码。middleware2
功能依次调用final
,执行后控制返回middleware2
,T5 完成任务后控制返回middleware1
。
输出将如图 8 所示。
图 8:中间件示例 1 的输出
图 8 提供了一个完全人为的例子,但是它用来说明中间件能给你多少控制。
Go 内部经常使用中间件。例如,net/http
包中的许多函数,比如StripPrefix
,都是中间件的教科书式例子——它们包装你的处理程序,并对请求或响应执行额外的操作。
中间件的概念可能有些难以理解,所以代码清单 5 显示了另一个例子。
代码清单 5:中间件示例 2
package
main
import
(
"net/http"
)
type
AfterMiddleware struct {
handler
http.Handler
}
func (a
*AfterMiddleware) ServeHTTP(w
http.ResponseWriter, r *http.Request) {
a.handler.ServeHTTP(w,
r)
w.Write([]byte(" +++ Hello
from middleware! +++ "))
}
func
myHandler(w http.ResponseWriter, r *http.Request)
{
w.Write([]byte(" *** Hello
from myHandler! *** "))
}
func
main() {
mid
:= &AfterMiddleware{http.HandlerFunc(myHandler)}
println("Listening
on port 8999")
http.ListenAndServe(":8999", mid)
}
在这段代码中,我们希望我们的中间件在myHandler
函数写入响应体时执行,并向其中添加一些数据。
首先,我们为中间件创建一个类型,我们称之为AfterMiddleware
。这由一个字段组成——我们希望我们的中间件响应的http.Handler
。
http.Handler
接口只需要我们实现ServeHTTP
接口:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
我们按如下方式进行:
func (a *AfterMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
a.handler.ServeHTTP(w, r)
w.Write([]byte(" +++ Hello from middleware! +++ "))
}
现在,响应由myHandler
所写的内容组成,后面是图 9 中中间件的输出。
图 9:中间件示例 2 的输出
这里我们不能涵盖中间件的所有方面,但是它是一个非常强大的特性,非常值得研究。Mat Ryer 的这篇文章和视频讲座是对该主题的极好介绍。
当我们创建一些非常有用的日志中间件时,我们将在第 7 章重新讨论中间件的概念。
Go 内置的net/http
包提供的路由功能只能让你做到这一步。如果可能的网址范围很复杂,例如,如果您希望能够使用正则表达式或变量来匹配网址,您可能会考虑第三方解决方案。
大猩猩网络工具包就是这样一个解决方案。大猩猩由 22 个包装组成,包括:
gorilla/context
存储全局请求变量。gorilla/mux
是功能强大的 URL 路由器和调度器。gorilla/reverse
为基于正则表达式的 mux 生成可逆的正则表达式。gorilla/rpc
通过 JSON-RPC 的编解码器实现 HTTP 上的 RPC。gorilla/schema
将表单值转换为结构。gorilla/securecookie
编码和解码经过身份验证和可选加密的 cookie 值。gorilla/sessions
保存 cookie 和文件系统会话,并允许自定义会话后端。gorilla/websocket
实现 RFC 6455 中定义的 WebSocket 协议。
从包列表中可以看出,Gorilla 不仅仅是 Go 路由和服务的替代多路复用器。事实上,它提供了一系列不同的工具来帮助您进行 web 开发。
但在这里,我们对多路复用器感兴趣。gorilla/mux
模块实现http.Handler
接口,使其与标准的 Golang 兼容。http.serveMux
。
此外,gorilla/mux
模块使您能够:
- 基于 URL 主机、路径、路径前缀、方案、头和查询值、HTTP 方法或您定义的自定义匹配器来匹配请求。
- 注册网址,以便您可以构建或“反转”它们,从而维护对资源的引用。
- 使用“子计算机”——仅当父路由匹配时才测试的嵌套路由。通过这种方式,您可以定义路由的“组”,这些路由都有一些共同点,例如主机或路径前缀。这通过仅在某些测试适合组的情况下进行测试来优化请求匹配(而不是对所有传入的请求执行测试)。
如果你只对 Gorilla 提供的更高级的路由功能感兴趣,你可以使用go get
在你的GOPATH
中安装gorilla/mux
。例如,假设您安装了git
:
go get github.com/gorilla/mux
接下来,您必须将其导入到应用程序中,如下所示:
package main
import ( github.com/gorilla/mux ... )
为了好玩,让我们使用gorilla/mux
创建一个基于正则表达式匹配产品标识的传入请求的处理程序。如果产品标识是一位数长,我们有一个匹配,可以路由到适当的页面。如果没有,我们将路由到错误页面。
我们首先需要导入gorilla/mux
模块,这是以正常方式进行的:
import( . . . github.com/gorilla/mux )
接下来,我们需要告诉 Go 使用gorilla/mux
而不是它自己的DefaultServeMux
:
func main() { router = mux.NewRouter() }
完成后,我们可以使用我们熟悉的处理函数,但是要在router
的上下文中使用gorilla/mux
提供的额外功能,例如使用正则表达式搜索 URL 参数:
router.HandleFunc("/product/{id:[0-9]+}", pageHandler)
这一行代码创建了一个处理函数,该函数试图匹配一个由从 0 到 9(包括 0 和 9)的数字/product/
组成的 URL,它称之为id
。如果有匹配,则调用pageHandler
函数。
在我们的pageHandler
函数中,我们需要一种方法来检查HandleFunc
匹配的确切字符串。我们可以使用 Gorilla 的mux.Vars
函数来做到这一点,将请求作为单个参数传入,并获取路线变量的映射。我们可以通过名称来引用我们感兴趣的路由变量— id
。
检索到产品标识后,我们接下来可以检查匹配的 HTML 页面。Go 的os
模块提供了Stat
和IsNotExist
功能来帮助我们。如果文件存在于服务器上,我们使用http.ServeFile
将其发送到浏览器。如果那里不存在,我们将返回另一个页面,告诉用户产品无效。
代码清单 6 显示了完整的代码。
代码清单 6:使用 gorilla/mux 服务和路由
package
main
import
(
"log"
"net/http"
"os"
"github.com/gorilla/mux"
)
func
pageHandler(w http.ResponseWriter, r *http.Request)
{
vars :=
mux.Vars(r)
productID
:= vars["id"]
log.Printf("Product ID:%v\n", productID)
fileName
:= productID +
".html"
if _, err :=
os.Stat(fileName); os.IsNotExist(err) {
log.Printf("no such product")
fileName = "invalid.html"
}
http.ServeFile(w,
r, fileName)
}
func
main() {
router
:= mux.NewRouter()
router.HandleFunc("/product/{id:[0-9]+}", pageHandler)
http.Handle("/", router)
http.ListenAndServe(":8999", nil)
}
这只是gorilla/mux
能做的事情的表面。例如,您可以匹配:
路径前缀:
router.PathPrefix("/products/")
HTTP 方法:
router.Methods("GET", "POST")
网址方案:
router.Schemes("https")
头值(例如,请求是 AJAX 请求吗?):
router.Headers("X-Requested-With", "XMLHttpRequest")
查询值:
router.Queries("key", "value")
您定义的自定义匹配函数:
router.MatcherFunc(func(r *http.Request, match *RouteMatch) bool {
// do something
})
您也可以通过链接函数调用将多个匹配器组合在一条路径中:
router.HandleFunc("/products", productHandler).
Host("www.example.com").
Methods("GET").
Schemes("https")
或者,您可以使用子路由将多条路由组合在一起。例如,下面的代码寻找一个名为www.example.com的主机名,但是它只有在主机名匹配的情况下才会检查子路由:
router := mux.NewRouter()
subrouter := route.Host("www.example.com").Subrouter()
//注册子行
subrouter.HandleFunc("/products/", AllProductsHandler)
subrouter.HandleFunc("/products/{name}", ProductHandler)
subrouter.HandleFunc("/reviews/{category}/{id:[0-9]+}"), ReviewsHandler)
gorilla/mux
包是DefaultServeMux
的一个非常有能力的替代品,非常值得研究。您可以在http://www.gorillatoolkit.org/pkg/mux了解更多信息。
当然,围棋是一个充满活力的生态系统,如果你不喜欢大猩猩,还有很多其他选择。我更喜欢大猩猩,因为它很容易让我分心,它总是能做我让它做的任何事情。
其他流行的第三方路由器包括:
httprouter:
快如闪电,但能力不及大猩猩。例如,您不能在路由中包含正则表达式。Httptreemux:
快速、灵活、基于树的 HTTP 路由器,灵感来自httprouter
。Pat:
使用简单,相当受欢迎。如果你是熟悉西纳特拉和 Rails 的 Ruby 主义者,你会发现 Pat 的方法非常熟悉。
事情并不总是如我们所愿。对不再存在的页面进行请求,将东西从一个地方移动到另一个地方,有时连接会中断。
HTTP 协议定义了 61 个不同的状态代码,这样我们就可以确定一个请求是否成功。
以下是一些最常见的例子:
200 OK
:万岁!一切都好。404 Not found
:无论你在找什么资源,在这里都找不到。301 Moved Permanently
:用作永久重定向到另一个页面。301 Moved Temporarily
:用作临时重定向到另一个页面。500 Internal Server Error
:出现了意想不到的问题。这是一个“包罗万象”
net/http
包提供了一个名为Error
的功能,您可以使用它来处理错误状态代码。作为开发人员,你的工作是选择一个对你所报告的错误类型有意义的。
您可以通过传递ResponseWriter
、字符串消息和状态代码来引发错误。例如,这会引发404 Not Found
错误:
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Something has gone wrong", 500)
})
然而,更好的做法是使用net/http
为此目的提供的各种辅助函数,而不是通用的http.Error()
函数。
例如:
// Return 404 Not Found http.NotFound(w, req)
`// Return 301 Permanently Moved http.Redirect(w, req, “http://somewhereelse.com”, 301)
// Return 302 Temporarily Moved http.Redirect(w, req “http://temporarylocation”, 302)`