Golang之接口型函数和http.Handler接口

一、接口型函数

参考Golang必备技巧:接口型函数

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的所有行为包在中间的目的。


转载:https://www.jianshu.com/p/7b3dc506c0e0

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
引用中提到了http.Handler的ServeHTTP方法。在这个方法中,首先检查handler是否为空,如果为空,则使用默认的处理程序DefaultServeMux来处理请求。这意味着http.Handler是一个接口,它定义了ServeHTTP方法,并且可以根据需要被替换为不同的处理程序。在引用的示例代码中,自定义的myHandler实现了http.Handler接口,并定义了ServeHTTP方法来处理不同的URL路径。在main函数中,将自定义的myHandler作为参数传递给http.ListenAndServe函数来处理HTTP请求。这样,当有请求到达时,会根据URL路径来选择相应的处理程序。这种方式使得web服务器能够将请求分派给任意的http.Handler,而不需要考虑其具体类。这也体现了在Go语言中,满足同一个接口的不同类是可以替换的。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [golang学习之go web 开发http.HandleFunc、http.ListenAndServe与http.Handler](https://blog.csdn.net/weixin_56349119/article/details/126335261)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *3* [Go语言圣经 - 第7章 接口 - 7.7 http.Handler 接口](https://blog.csdn.net/weixin_51487151/article/details/122318673)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值