Golang 接口型函数和http.Handler接口
一、接口型函数
1.原始接口实现
type Handler interface {
Do(k, v interface{})
}
func Each(m map[interface{}]interface{}, h Handler) {
if m != nil && len(m) > 0 {
for k, v := range m {
h.Do(k, v)
}
}
}
这里具体要做什么,由实现Handler接口的类型自己去定义。也就是Each实现了面向接口编程。比如:
type welcome string
func (w welcome) Do(k, v interface{}) {
fmt.Printf("%s,我叫%s,今年%d岁\n", w,k, v)
}
func main() {
persons := make(map[interface{}]interface{})
persons["张三"] = 20
persons["李四"] = 23
persons["王五"] = 26
var w welcome = "大家好"
Each(persons, w)
}
以上实现,我们定义了一个map来存储学生们,map的key是学生的名字,value是该学生的年龄。welcome是我们新定义的类型,对应基本类型string,该welcome实现了Handler接口,打印出自我介绍。
2.接口型函数出场
以上实现,主要有两点不太好:
- 因为必须要实现Handler接口,Do这个方法名不能修改,不能定义一个更有意义的名字
- 必须要新定义一个类型,才可以实现Handler接口,才能使用Each函数
首先我们先解决第一个问题,根据我们具体做的事情定义一个更有意义的方法名,比如例子中是自我介绍,那么使用selfInfo要比Do这个干巴巴的方法要好的多。
如果调用者改了方法名,那么就不能实现Handler接口,还要使用Each方法怎么办?那就是由提供Each函数的负责提供Handler的实现,我们添加代码如下:
type HandlerFunc func(k, v interface{})
func (f HandlerFunc) Do(k, v interface{}){
f(k,v)
}
type welcome string
func (w welcome) selfInfo(k, v interface{}) {
fmt.Printf("%s,我叫%s,今年%d岁\n", w,k, v)
}
func main() {
persons := make(map[interface{}]interface{})
persons["张三"] = 20
persons["李四"] = 23
persons["王五"] = 26
var w welcome = "大家好"
Each(persons, HandlerFunc(w.selfInfo))
}
还是差不多原来的实现,只是把方法名Do改为selfInfo。HandlerFunc(w.selfInfo)不是方法的调用,而是转型,因为selfInfo和HandlerFunc是同一种类型,所以可以强制转型。转型后,因为HandlerFunc实现了Handler接口,所以我们就可以继续使用原来的Each方法了。
3.进一步重构
现在解决了命名的问题,但是每次强制转型不太好,我们继续重构,可以采用新定义一个函数的方式,帮助调用者强制转型。
func EachFunc(m map[interface{}]interface{}, f func(k, v interface{})) {
Each(m,HandlerFunc(f))
}
...
EachFunc(persons, w.selfInfo)
新增了一个EachFunc函数,帮助调用者强制转型,调用者就不用自己做了。
现在我们发现EachFunc函数接收的是一个func(k, v interface{})类型的函数,没有必要实现Handler接口了,所以我们新的类型可以去掉不用了。
func selfInfo(k, v interface{}) {
fmt.Printf("大家好,我叫%s,今年%d岁\n", k, v)
}
func main() {
persons := make(map[interface{}]interface{})
persons["张三"] = 20
persons["李四"] = 23
persons["王五"] = 26
EachFunc(persons, selfInfo)
}
去掉了自定义类型welcome之后,整个代码更简洁,可读性更好。我们的方法含义都是:
- 让这学生自我介绍
- 让这些学生起立
- 让这些学生早读
- 让这些学生…
都是这种默认,方法处理,更符合自然语言规则。
4.总结
以上关于函数型接口就写完了,如果我们仔细留意,发现和我们自己平时使用的http.Handle方法非常像,其实接口http.Handler就是这么实现的。
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
func Handle(pattern string, handler Handler) {
DefaultServeMux.Handle(pattern, handler)
}
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}
这是一种非常好的技巧,提供两种函数,既可以以接口的方式使用,也可以以方法的方式,对应我们例子中的Each和EachFunc这两个函数,灵活方便。
二、http.Handler接口
摘自《GO语言圣经》第7章
net/http:
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并输出物品信息。
func main() {
db := database{"shoes": 50, "socks": 5}
log.Fatal(http.ListenAndServe("localhost:8000", db))
}
type dollars float32
func (d dollars) String() string { return fmt.Sprintf("$%.2f", d) }
type database map[string]dollars
func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
for item, price := range db {
fmt.Fprintf(w, "%s: %s\n", item, price)
}
}
如果我们启动这个服务,然后用web浏览器来连接localhost:8000,我们得到下面的输出:
shoes: $50.00
socks: $5.00
目前为止,这个服务器不考虑URL只能为每个请求列出它全部的库存清单。更真实的服务器会定义多个不同的URL,每一个都会触发一个不同的行为。让我们使用/list来调用已经存在的这个行为并且增加另一个/price调用表明单个货品的价格,像这样/price?item=socks来指定一个请求参数。
func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
switch req.URL.Path {
case "/list":
for item, price := range db {
fmt.Fprintf(w, "%s: %s\n", item, price)
}
case "/price":
item := req.URL.Query().Get("item")
price, ok := db[item]
if !ok {
w.WriteHeader(http.StatusNotFound) // 404
fmt.Fprintf(w, "no such item: %q\n", item)
return
}
fmt.Fprintf(w, "%s\n", price)
default:
w.WriteHeader(http.StatusNotFound) // 404
fmt.Fprintf(w, "no such page: %s\n", req.URL)
}
}
现在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/ch1/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
二、ServeMux
显然我们可以继续向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。
func main() {
db := database{"shoes": 50, "socks": 5}
mux := http.NewServeMux()
mux.Handle("/list", http.HandlerFunc(db.list))
mux.Handle("/price", http.HandlerFunc(db.price))
log.Fatal(http.ListenAndServe("localhost:8000", mux))
}
type database map[string]dollars
func (db database) list(w http.ResponseWriter, req *http.Request) {
for item, price := range db {
fmt.Fprintf(w, "%s: %s\n", item, price)
}
}
func (db database) price(w http.ResponseWriter, req *http.Request) {
item := req.URL.Query().Get("item")
price, ok := db[item]
if !ok {
w.WriteHeader(http.StatusNotFound) // 404
fmt.Fprintf(w, "no such item: %q\n", item)
return
}
fmt.Fprintf(w, "%s\n", price)
}
让我们关注这两个注册到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方法等等。
这里原书说的有点绕,说一下个人的理解,先看一下使用方式
mux := http.NewServeMux()
mux.Handle("/list", http.HandlerFunc(db.list))
...
func (mux *ServeMux) Handle(pattern string, handler Handler) {
可以看到Handle这个方法,要求传入一个Handler接口类型,上文分析这个接口类型需要实现ServeHTTP(w ResponseWriter, r *Request)
即可。但是现在我们不想传一个实现这种接口的类型,而是想传入一个方法,并且这个方法干的事情和ServeHTTP一样,连参数也一样。这就像一个电源适配器一样,只是改改插孔,这个适配器是这样的:
package http
type HandlerFunc func(w ResponseWriter, r *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
因为handler通过这种方式注册非常普遍,ServeMux有一个方便的HandleFunc方法,它帮我们简化handler注册代码成这样:
mux.HandleFunc("/list", db.list)
mux.HandleFunc("/price", db.price)
从上面的代码很容易看出应该怎么构建一个程序,它有两个不同的web服务器监听不同的端口的,并且定义不同的URL将它们指派到不同的handler。我们只要构建另外一个ServeMux并且在调用一次ListenAndServe(可能并行的)。但是在大多数程序中,一个web服务器就足够了。
此外,在一个应用程序的多个文件中定义HTTP handler也是非常典型的,如果它们必须全部都显示的注册到这个应用的ServeMux实例上会比较麻烦。所以为了方便,net/http包提供了一个全局的ServeMux实例DefaultServerMux和包级别的http.Handle和http.HandleFunc函数。现在,为了使用DefaultServeMux作为服务器的主handler,我们不需要将它传给ListenAndServe函数;nil值就可以工作。然后服务器的主函数可以简化成:
func main() {
db := database{"shoes": 50, "socks": 5}
http.HandleFunc("/list", db.list)
http.HandleFunc("/price", db.price)
log.Fatal(http.ListenAndServe("localhost:8000", nil))
}
最后,一个重要的提示:就像我们在1.7节中提到的,web服务器在一个新的协程中调用每一个handler,所以当handler获取其它协程或者这个handler本身的其它请求也可以访问的变量时一定要使用预防措施比如锁机制。
// Server2 is a minimal "echo" and counter server.
package main
import (
"fmt"
"log"
"net/http"
"sync"
)
var mu sync.Mutex
var count int
func main() {
http.HandleFunc("/", handler)
http.HandleFunc("/count", counter)
log.Fatal(http.ListenAndServe("localhost:8000", nil))
}
// handler echoes the Path component of the requested URL.
func handler(w http.ResponseWriter, r *http.Request) {
mu.Lock()
count++
mu.Unlock()
fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
}
// counter echoes the number of calls so far.
func counter(w http.ResponseWriter, r *http.Request) {
mu.Lock()
fmt.Fprintf(w, "Count %d\n", count)
mu.Unlock()
}
这个服务器有两个请求处理函数,根据请求的url不同会调用不同的函数:对/count这个url的请求会调用到count这个函数,其它的url都会调用默认的处理函数。如果你的请求pattern是以/结尾,那么所有以该url为前缀的url都会被这条规则匹配。在这些代码的背后,服务器每一次接收请求处理时都会另起一个goroutine,这样服务器就可以同一时间处理多个请求。然而在并发情况下,假如真的有两个请求同一时刻去更新count,那么这个值可能并不会被正确地增加;这个程序可能会引发一个严重的bug:竞态条件(参见9.1)。为了避免这个问题,我们必须保证每次修改变量的最多只能有一个goroutine,这也就是代码里的mu.Lock()和mu.Unlock()调用将修改count的所有行为包在中间的目的。
下一篇: C# 获取当前月份天数的三种方法总结