http.Handler接口
在第一章中,我们粗略的了解了怎么用 net/http 包去实现网络客户端(§1.5)和服务器(§1.7)。在这个小节中,我们会对那些基于 http.Handler 接口的服务器 API 做更进一步的学习:
package http
type Handler interface {
ServeHTTP(w ResponseWriter, r *Request)
}
func ListenAndServe(address string, h Handler) error
ListenAndServe 函数需要一个例如 “localhost:8000” 的服务器地址,和一个所有请求都可以分派的 Handler 接口实例。它会一直运行,直到这个服务因为一个错误而失败(或者启动失败),它的返回值一定是一个非空的错误。
想象一个电子商务网站,为了销售,将数据库中物品的价格映射成美元。下面这个程序可能是能想到的最简单的实现了。它将库存清单模型化为一个命名为 database 的 map 类型,我们给这个类型一个 ServeHttp 方法,这样它可以满足 http.Handler 接口。这个 handler 会遍历整个 map 并输出物品信息。
Unresolved include directive in modules/ROOT/pages/ch7/ch7-07.adoc - include::example$/ch7/http1/main.go[]
如果我们启动这个服务,
$ go build gopl.io/ch7/http1
$ ./http1 &
然后用1.5节中的获取程序(如果你更喜欢可以使用 web 浏览器)来连接服务器,我们得到下面的输出:
$ go build gopl.io/modules/fetch
$ ./fetch http://localhost:8000
shoes: $50.00
socks: $5.00
目前为止,这个服务器不考虑 URL,只能为每个请求列出它全部的库存清单。更真实的服务器会定义多个不同的 URL,每一个都会触发一个不同的行为。让我们使用 /list 来调用已经存在的这个行为并且增加另一个 /price 调用表明单个货品的价格,像这样 /price?item=socks 来指定一个请求参数。
Unresolved include directive in modules/ROOT/pages/ch7/ch7-07.adoc - include::example$/ch7/http2/main.go[]
现在 handler 基于 URL 的路径部分(req.URL.Path)来决定执行什么逻辑。如果这个 handler 不能识别这个路径,它会通过调用 w.WriteHeader(http.StatusNotFound) 返回客户端一个 HTTP 错误;这个检查应该在向 w 写入任何值前完成。(顺便提一下,http.ResponseWriter 是另一个接口。它在 io.Writer 上增加了发送 HTTP 相应头的方法。)等效地,我们可以使用实用的 http.Error 函数:
msg := fmt.Sprintf("no such page: %s\n", req.URL)
http.Error(w, msg, http.StatusNotFound) // 404
/price 的 case 会调用 URL 的 Query 方法来将 HTTP 请求参数解析为一个 map,或者更准确地说一个 net/url 包中 url.Values (§6.2.1)类型的多重映射。然后找到第一个 item 参数并查找它的价格。如果这个货品没有找到会返回一个错误。
这里是一个和新服务器会话的例子:
$ go build gopl.io/ch7/http2
$ go build gopl.io/modules/fetch
$ ./http2 &
$ ./fetch http://localhost:8000/list
shoes: $50.00
socks: $5.00
$ ./fetch http://localhost:8000/price?item=socks
$5.00
$ ./fetch http://localhost:8000/price?item=shoes
$50.00
$ ./fetch http://localhost:8000/price?item=hat
no such item: "hat"
$ ./fetch http://localhost:8000/help
no such page: /help
显然我们可以继续向 ServeHTTP 方法中添加 case,但在一个实际的应用中,将每个 case 中的逻辑定义到一个分开的方法或函数中会很实用。此外,相近的 URL 可能需要相似的逻辑;例如几个图片文件可能有形如 /images/*.png 的 URL 。因为这些原因,net/http 包提供了一个请求多路器 ServeMux 来简化 URL 和 handlers 的联系。一个 ServeMux 将一批 http.Handler 聚集到一个单一的 http.Handler 中。再一次,我们可以看到满足同一接口的不同类型是可替换的:web 服务器将请求指派给任意的 http.Handler 而不需要考虑它后面的具体类型。
对于更复杂的应用,一些 ServeMux 可以通过组合来处理更加错综复杂的路由需求。Go 语言目前没有一个权威的 web 框架,就像 Ruby 语言有 Rails 和 python 有 Django 。这并不是说这样的框架不存在,而是 Go 语言标准库中的构建模块就已经非常灵活以至于这些框架都是不必要的。此外,尽管在一个项目早期使用框架是非常方便的,但是它们带来额外的复杂度会使长期的维护更加困难。
在下面的程序中,我们创建一个 ServeMux 并且使用它将 URL 和相应处理 /list 和 /price 操作的 handler 联系起来,这些操作逻辑都已经被分到不同的方法中。然后我们在调用 ListenAndServe 函数中使用 ServeMux 为主要的 handler。
Unresolved include directive in modules/ROOT/pages/ch7/ch7-07.adoc - include::example$/ch7/http3/main.go[]
让我们关注这两个注册到 handlers 上的调用。第一个 db.list 是一个方法值(§6.4),它是下面这个类型的值。
func(w http.ResponseWriter, req *http.Request)
也就是说 db.list 的调用会援引一个接收者是 db 的 database.list 方法。所以 db.list 是一个实现了 handler 类似行为的函数,但是因为它没有方法(理解:该方法没有它自己的方法),所以它不满足 http.Handler 接口并且不能直接传给 mux.Handle 。
语句 http.HandlerFunc(db.list) 是一个转换而非一个函数调用,因为 http.HandlerFunc 是一个类型。它有如下的定义:
package http
type HandlerFunc func(w ResponseWriter, r *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
HandlerFunc 显示了在 Go 语言接口机制中一些不同寻常的特点。这是一个实现了接口 http.Handler 的方法的函数类型。ServeHTTP 方法的行为是调用了它的函数本身。因此 HandlerFunc 是一个让函数值满足一个接口的适配器,这里函数和这个接口仅有的方法有相同的函数签名。实际上,这个技巧让一个单一的类型例如 database 以多种方式满足 http.Handler 接口:一种通过它的 list 方法,一种通过它的 price 方法等等。
因为 handler 通过这种方式注册非常普遍,ServeMux 有一个方便的 HandleFunc 方法,它帮我们简化 handler 注册代码成这样:
Unresolved include directive in modules/ROOT/pages/ch7/ch7-07.adoc - include::example$/ch7/http3a/main.go[]
从上面的代码很容易看出应该怎么构建一个程序:由两个不同的 web 服务器监听不同的端口,并且定义不同的 URL 将它们指派到不同的 handler 。我们只要构建另外一个 ServeMux 并且再调用一次 ListenAndServe (可能并行的)。但是在大多数程序中,一个 web 服务器就足够了。此外,在一个应用程序的多个文件中定义 HTTP handler 也是非常典型的,如果它们必须全部都显式地注册到这个应用的 ServeMux 实例上会比较麻烦。
所以为了方便,net/http 包提供了一个全局的 ServeMux 实例 DefaultServerMux 和包级别的 http.Handle 和 http.HandleFunc 函数。现在,为了使用 DefaultServeMux 作为服务器的主 handler,我们不需要将它传给 ListenAndServe 函数;nil 值就可以工作。
然后服务器的主函数可以简化成:
Unresolved include directive in modules/ROOT/pages/ch7/ch7-07.adoc - include::example$/ch7/http4/main.go[]
最后,一个重要的提示:就像我们在1.7节中提到的,web 服务器在一个新的协程中调用每一个 handler ,所以当 handler 获取其它协程或者这个 handler 本身的其它请求也可以访问到变量时,一定要使用预防措施,比如锁机制。我们后面的两章中将讲到并发相关的知识。
练习 7.11: 增加额外的 handler 让客户端可以创建,读取,更新和删除数据库记录。例如,一个形如 /update?item=socks&price=6 的请求会更新库存清单里一个货品的价格并且当这个货品不存在或价格无效时返回一个错误值。(注意:这个修改会引入变量同时更新的问题)
练习 7.12: 修改 /list 的 handler 让它把输出打印成一个 HTML 的表格而不是文本。html/template包(§4.6)可能会对你有帮助。