Go 学习笔记(三十)Handler

本文原创地址:博客园骏马金龙Go Web:Handler

Multiplexer 根据 URL 将请求路由给指定的 Handler。 Handler 用于处理请求并给予响应。更严格地说,用来读取请求体、并将请求对应的响应字段 (respones header) 写入 ResponseWriter 中,然后返回

1.png

什么是 Handler

什么是 Handler。它是一个接口,定义在 net/http/server.go 中:

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

也就是说,实现了 ServerHTTP 方法的都是 Handler。注意 ServerHTTP 方法的参数:http.ResponesWriter 接口和 Request 指针。

在 Handler 的注释中,给出了几点主要的说明:

  1. Handler 用于响应一个 HTTP request
  2. 接口方法 ServerHTTP 应该用来将 response header 和需要响应的数据写入到 ResponseWriter 中,然后返回。返回意味着这个请求已经处理结束,不能再使用这个 ResponseWriter、不能再从 Request.Body 中读取数据,不能并发调用已完成的 ServerHTTP 方法
  3. handler 应该先读取 Request.Body,然后再写 ResponseWriter。只要开始向 ResponseWriter 写数据后,就不能再从 Request.Body 中读取数据
  4. handler 只能用来读取 request 的 body,不能修改已取得的 Request(因为它的参数 Request 是指针类型的)

ResponseWriter 接口说明

再看看 ResponseWriter 接口的定义:

// A ResponseWriter interface is used by an HTTP handler to
// construct an HTTP response.
//
// A ResponseWriter may not be used after the Handler.ServeHTTP method
// has returned.
type ResponseWriter interface {
	Header() Header
	Write([]byte) (int, error)
	WriteHeader(statusCode int)
}

注释中已经说明, ResponseWriter 接口的作用是用于构造 HTTP response 。且明确指定了 Handler.ServerHTTP 方法返回以后就不能再使用 ResponseWriter 了。

这个接口有 3 个方法:

  • Header() 方法 用来构造响应 Header,它返回一个 Header 对象,这个 Header 对象稍后将被 WriterHeader() 响应出去。Header 类型是一个 map 类型的结构,字段名为 key、字段值为 value:
    type Header map[string][]string
  • Write() 方法 用于向网络连接中写响应数据。
  • WriteHeader() 方法 将给定的响应状态码和响应 Header 一起发送出去。

很显然, ResponseWriter 的作用是构造响应 header,并将响应 header 和响应数据通过网络链接发送给客户端

再看 ListenAndServe()

在启动 go http 自带的 web 服务时,调用了函数 ListenAndServe()。这个函数的定义如下:

func ListenAndServe(addr string, handler Handler) error

该函数有两个参数,第一个参数是自带的 web 监听地址和端口,第二个参数是 Handler,用来处理每个接进来的 http request,但一般第二个参数设置为 nil,表示调用默认的 Multiplexer:DefaultServeMux。这个默认的 ServeMux 唯一的作用,是将请求根据 URL 路由给对应的 handler 进行处理。

var DefaultServeMux = &defaultServeMux

这里有两个问题:

  • (1). 第二个参数为什么建议设置为 nil
  • (2). 设置为 nil 后,DefaultServeMux 是请求的路由器,它为什么可以充当一个 handler

先看第二个问题,很简单,因为 ServeMux 类型定义了 ServeHTTP() 方法,它实现了 Handler 接口:

type ServeMux struct {
		// Has unexported fields.
}
func NewServeMux() *ServeMux
func (mux *ServeMux) Handle(pattern string, handler Handler)
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request))
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string)
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request)

前面说过,只要实现了 ServerHTTP() 方法的类型,就是一个 Handler。而 DefaultServeMux 是默认的 ServeMux,所以它是一个 Handler。

关于第一个问题,看一个示例就知道了。

package main

import (
	"fmt"
	"net/http"
)

// MyHandler实现Handler接口
type MyHandler struct{}

func (h *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello World!\n")
}

func main() {
	handler := MyHandler{}
	server := http.Server{
		Addr:    "127.0.0.1:8080",
		Handler: &handler,      // 以&handler作为第二个参数
	}
	server.ListenAndServe()
}

上面的示例中定义了一个 handler,它实现的 ServeHTTP() 方法只有一个作用,输出Hello World!。并且将这个 handler 作为 ListenAndServe() 的第二个参数。

注意,上面以&handler作为参数而非handler,因为此处 MyHandler 中实现的 ServerHTTP() 方法的 receiver 是指针类型的,所以 MyHandler 的实例对象也必须是指针类型的,如此才能实现 Handler 接口。

启动这个 web 服务后,以不同的 URL 去访问它,将总是得到完全相同的响应结果:

2.png

很显然, 当 handler 作为 ListenAndServe() 的第二个参数时,任意请求都会使用这个唯一的 handler 进行处理

所以,建议将第二个参数设置为 nil(或者上面的 Serve Struct 不指定 Handler 字段),它表示调用默认的 DefaultServeMux 作为 handler,使得每个访问请求都会调用这个特殊的 handler,而这个 handler 的作用是将请求根据 url 路由给不同的 handler。

另外需要注意的是, http 包中提供的 Handle()和 HandleFunc() 函数其实是 DefaultServeMux.XXX 的封装,所以直接调用 http.Handle()和 http.HandleFunc() 实际上是在调用 DefaultServeMux.Handle()和 DefaultServeMux.HandleFunc()

func Handle(pattern string, handler Handler) {
	DefaultServeMux.Handle(pattern, handler)
}

func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	DefaultServeMux.HandleFunc(pattern, handler)
}

使用 DefaultServeMux 后的 Handler 示例

下面是使用了 DefaultServeMux 的示例。

创建了两个 handler,一个 handler 用于对应/hello,该 handler 用于输出Hello,另一个 handler 用于对应world,该 handler 用于输出World

package main

import (
	"fmt"
	"net/http"
)

type HelloHandler struct{}
type WorldHandler struct{}

func (h *HelloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello\n")
}
func (h *WorldHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "World\n")
}

func main() {
	helloHandler := HelloHandler{}
	worldHandler := WorldHandler{}
	server := http.Server{
		Addr:    "127.0.0.1:8080",
	}
	http.Handle("/hello",&helloHandler)
	http.Handle("/world",&worldHandler)
	server.ListenAndServe()
}

下面是访问的结果:

3.png

HandleFunc 是什么

除了使用 Handle 处理 http 请求,也能使用 HandleFunc() 处理。

先看一个使用 HandleFunc() 处理请求的示例,示例的效果和前文是一样的。

package main

import (
	"fmt"
	"net/http"
)

func hello(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello\n")
}

func world(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "World\n")
}

func main() {
	server := http.Server{
		Addr: "127.0.0.1:8080",
	}
	http.HandleFunc("/hello", hello)
	http.HandleFunc("/world", world)

	server.ListenAndServe()
}

下面是访问的结果:

4.png

Go 有一个函数 HandleFunc(),它表示 使用第二个参数的函数作为 handler ,处理匹配到的 url 路径请求。

func HandleFunc(pattern string, handler func(ResponseWriter, *Request))

不难看出,HandleFunc()使得我们可以直接使用一个函数作为 handler,而不需要自定义一个实现 Handler 接口的类型。正如上面的示例中,我们没有定义 Handler 类型,也没有去实现 ServeHTTP() 方法,而是直接定义函数,并将其作为 handler。

换句话说,HandleFunc()使得我们可以更简便地为某些 url 路径注册 handler。但是,使用 HandleFunc() 毕竟是图简便,有时候不得不使用 Handle(),比如我们确定要定义一个 type。

Handle()、HandleFunc() 和 Handler、HandlerFunc 的关系

说实话,一开始感觉挺乱的。

Handle()和 HandleFunc() 是函数,用来给 url 绑定 handler。Handler 和 HandlerFunc 类型,用来处理请求

看 Handle()、HandleFunc() 以及 Handler、HandlerFunc 的定义就已经很清晰了:

func Handle(pattern string, handler Handler) {}
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {}

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request)

Handle()和 HandleFunc() 都是为某个 url 路径模式绑定一个对应的 handler,只不过 HandleFunc()是直接使用函数作为 handler,而 Handle() 是使用 Handler 类型的实例作为 handler。

Handler 接口的实例都实现了 ServeHTTP() 方法,都用来处理请求并响应给客户端。

HandlerFunc 类型不是接口,但它有一个方法 ServeHTTP(),也就是说 HandlerFunc 其实也是一种 Handler

因为 HandlerFunc 是类型,只要某个函数的签名是func(ResponseWriter, *Request),它就是 HandlerFunc 类型的一个实例。另一方面,这个类型的实例 (可能是参数、可能是返回值类型) 可以和某个签名为func(ResponseWriter, *Request)的函数进行互相赋值 。这个过程可能很隐式,但确实经常出现相关的用法。

例如:

// 一个函数类型的handler
func myhf(ResponseWriter, *Request){}

// 以HandlerFunc类型作为参数类型
func a(hf HandlerFunc){}

// 所以,可以将myhf作为a()的参数
a(myhf)

实际上, 可以使用 HandlerFunc()进行转换。例如有一个函数 world(),它的参数是合理的,使用 HandlerFunc(world) 表示将其转换为一个 Handler 。这个转换、适应在后面会经常用到。

例如:

// 两个函数
func hello(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello\n")
}

func world(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "World\n")
}

func main() {
	server := http.Server{
		Addr: "127.0.0.1:8080",
	}
	// 第一个使用HandleFunc()为路径注册hello()函数handler
	// 第二个使用Handle()为路径注册转换后的handler
	http.HandleFunc("/hello", hello)
	http.Handle("/world", http.HandlerFunc(world))

	server.ListenAndServe()
}

上面的示例中,Handle() 函数的第二个参数要求的是 Handler 类型,使用http.HandlerFunc(world)就将函数 world() 转换成了 Handler 类型的一个实例。

链式 handler

handler 是用来处理 http 请求的,处理过程可能会很简单,也可能会很复杂。复杂的情况下,可能无法使用一个单独的 handler 来完成工作,毕竟 handler 只是一个函数。尽管我们可以直接在这个函数中调用其它函数。

很经常地,可能 handler 中需要嵌套其它 handler,甚至多层嵌套,这就是链式 handler。

由于 Handle()或 HandleFunc() 注册的时候需要指定参数类型,所以 handler 嵌套的时候,也要关注 handler 的参数类型以及返回类型 。看下面示例就会明白参数类型和返回类型是怎么要求的。

HandleFunc() 的嵌套示例

代码如下:

package main

import (
	"fmt"
	"net/http"
)

func hello(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello World!\n")
}

func log(hf http.HandlerFunc) http.HandlerFunc {
	count := 0
	return func(w http.ResponseWriter, r *http.Request) {
		count++
		fmt.Printf("Handler Function called %d times\n", count)
		hf(w, r)
	}
}

func main() {
	server := http.Server{
		Addr: "127.0.0.1:8080",
	}
	http.HandleFunc("/hello", log(hello))
	server.ListenAndServe()
}

多次访问http://127.0.0.1:8080/hello,将在浏览器中输出 "Hello World!",但同时会在运行这个 go 程序的终端上多次输出以下内容:

$ go run test.go
Handler Function called 1 times
Handler Function called 2 times
Handler Function called 3 times
Handler Function called 4 times
Handler Function called 5 times

上面的示例中,主要看下面两段代码:

func hello(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello World!\n")
}

func log(hf http.HandlerFunc) http.HandlerFunc {
	count := 0
	return func(w http.ResponseWriter, r *http.Request) {
		count++
		fmt.Printf("Handler Function called %d times\n", count)
		hf(w, r)
	}
}

hello()是一个普通的 HandlerFunc 类型函数,因为它的签名符合 HandlerFunc 类型,所以它是 HandlerFunc 类型的一个实例。而 log() 函数正是以 HandlerFunc 类型作为参数的,所以前面的示例代码中,将 hello 函数作为了 log 函数的参数:

http.HandleFunc("/hello", log(hello))

HandleFunc()的第二个参数要求是 HandlerFunc 类型的,所以 log() 的返回值是 HandlerFunc 类型。在 log()中,使用匿名函数作为它的返回值,这里的这个匿名函数是一个闭包 ( 因为引用了外层函数的变量 hf 和 count)。这个匿名函数最后调用hf(w,r),由于 hf 是 HandlerFunc 类型的一个实例,所以可以如此调用。

上面体现了 HandlerFunc 嵌套时候关于参数以及返回值的一些细节。

上面的示例中还有一个细节需要引起注意:为什么每次访问时,上面的 count 都会记住之前的值并自增,而不是重置为 0 后自增。

之所以有这个疑问,可能是认为每次访问时,请求处理完成后 handler 就退出了,闭包虽然会记住外层函数的自由变量 count,但也会因为处理完成后退出,导致每次访问都重置为 0 后自增。但实际上, handler 是注册在给定路径上的,只要 web 服务没有退出,这个 handler 就一直不会退出,不会因为每次访问都重新注册 handler 。所以,闭包 handler 一直引用着 hf 和 count 这两个自由变量。

HandlerFunc 嵌套 Handler

将上面的 HandlerFunc 嵌套 HandlerFunc 修改一下,变成 Handler 嵌套 HandlerFunc。

package main

import (
	"fmt"
	"net/http"
)

type MyHandler struct{}

func (wh *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello World!\n")
}

func log(h http.Handler) http.Handler {
	count := 0
	f := func(w http.ResponseWriter, r *http.Request) {
		count++
		fmt.Printf("Handler Function called %d times\n", count)
		h.ServeHTTP(w, r)
	}
	return http.HandlerFunc(f)
}

func main() {
	myHandler := MyHandler{}
	server := http.Server{
		Addr: "127.0.0.1:8080",
	}
	http.Handle("/hello", log(&myHandler))
	server.ListenAndServe()
}

逻辑也很简单,无非是将 HandlerFunc 转换成 Handler。

思考一下,Handler 是否可以嵌套 Handler,或者 Handler 嵌套 HandlerFunc。可以,但是很不方便,因为 ServeHTTP() 方法限制了没法调用其它的 Handler,除非定义的某个 Handler 是嵌套在某个 Handler 类型中的类型。
上一篇 Go 学习笔记(二十九)惰性数值生成器
Go 学习笔记(目录)
下一篇 Go 学习笔记(三十一)URLs