在进行Web应用开发的时候,使用成熟并且复杂的Web应用框架会使开发变得更加迅速和简便,但这意味着开发者必须接受框架自身的一套约定和模式。虽然很多框架都认为自己提供的约定和模式是最佳实践,但是如果开发者没有正确的理解这些最佳实践,那么对最佳实践的应用就可能会发展为货物崇拜编程:开发者如果不了解这些约定和模式的用法,就可能会在不必要甚至有害的情况下盲目的使用它们。—摘自《Go Web编程-黄健宏译》
对Go语言而言,隐藏在框架下的通常是net/http和html/template这两个标准库。
1 net/http标准库的各个组成部分
net/http标准库的各个组成部分
库中的结构和函数有些支持客户端和服务器中的一个,而有些则同时支持客户端和服务器:
- Client、Response、Header、Request和Cookie对客户端进行支持;
- Server、ServerMux、Handler/HandleFunc、ResponseWriter、Header、Request和Cookie则对服务器进行支持。
2 使用Go构建服务器
通过Go服务器处理请求
3 Go Web服务器
创建HTTP服务器的方式有两种:
- 方式一:http.ListenAndServe(“”,nil)
直接调用ListenAndServe()并传入【网络地址】和【负责请求的处理器】作为参数即可
【网络地址】如果为空字符串,那么服务器默认使用80端口进行网络链接
【负责请求的处理器】如果为nil,那么使用默认的多路复用器 DefaultServeMux
package main
import (
"net/http"
)
func main() {
err := http.ListenAndServe("", nil)
if err != nil {
return
}
}
创建http服务方式一
方式二:先借助Server结构对服务器进行更详细的配置,然后再调用ListenAndServe()创建服务器
package main
import (
"net/http"
)
func main() {
server := http.Server{
Addr: "127.0.0.1",
Handler: nil,
}
err := server.ListenAndServe()
if err != nil {
return
}
}
创建http服务方式二
方式一内部调用的是server.ListenAndServe(),综上,以后直接使用第二种方式。
ListenAndServe()源码:
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
ListenAndServe源码
DefaultServeMux的本质:
type Server struct{
...
Handler Handler // handler to invoke, http.DefaultServeMux if nil
...
}
// DefaultServeMux is the default ServeMux used by Serve.
var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux
type ServeMux struct {
mu sync.RWMutex
m map[string]muxEntry
es []muxEntry // slice of entries sorted from longest to shortest.
hosts bool // whether any patterns contain hostnames
}
server.go中揭示了DefaultServeMux的本质
4 处理器和处理器函数
4.1 处理器
一个处理器就是拥有一个ServerHTTP()方法的接口,并且这个ServerHTTP方法需要接受两个参数:第一个参数是ResponseWriter接口,第二个参数是一个指向Request结构的指针。
也就是说,任何接口只要拥有一个ServerHTTP方法,并且该方法带有以下签名,那么他就是一个处理器:
ServerHttp(http.ResponseWriter,*http.Request)
server.ListenAndServe方法第二个参数是处理器,但是其默认值却是多路复用器DefaultServeMux?
从DefaultServeMux的本质可得知,DefaultServeMux是ServeMux结构的一个实例,而ServeMux拥有ServerHTTP方法,并且这个方法的签名与成为处理器所需的签名完全一致。因此DefaultServeMux不仅是一个多路复用器,而且还是一个处理器。
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
if r.RequestURI == "*" {
if r.ProtoAtLeast(1, 1) {
w.Header().Set("Connection", "close")
}
w.WriteHeader(StatusBadRequest)
return
}
h, _ := mux.Handler(r)
h.ServeHTTP(w, r)
}
ServeMux结构的ServerHTTP方法
DefaultServeMux处理器和其他一般的处理器不同,**DefaultServeMux是一个特殊的处理器,它唯一要做的就是根据请求的URL将请求重定向到不同的处理器。**
4.1.1 自定义处理器
package main
import (
"fmt"
"net/http"
)
// 自定义处理器结构体
type MyHandler struct {}
// 要想成为一个处理器,必须实现ServerHTTP方法且参数也需要满足签名要求。
func (h *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w,"Hello World!")
}
func main() {
server := http.Server{
Addr: "127.0.0.1:8080",
Handler: &MyHandler{},
}
err := server.ListenAndServe()
if err != nil {
return
}
}
自定义处理器
在浏览器中请求
发现一个问题,无论请求的url如何变化,都会返回相同的响应
这也就从侧面说明了多路复用器的作用。
在程序中我们用自定义的Handler: &MyHandler{}处理器取代了默认的多路复用器-DefaultServeMux,这也就意味着多路复用器的功能被替换掉啦,进而使得服务器不会再通过URL匹配来将请求路由至不同的处理器,而是直接使用同一个处理器(Handler: &MyHandler{})来处理所有请求,因此,无论浏览器访问什么地址,服务器返回的都是同样的响应。
4.1.3 使用多个处理器
在实际情况中,通常都是使用多个处理器处理不同的URL。也就是说,我们不再在Server结构的Handle字段中指定处理器,而是让处理器使用默认的多路服务器DefaultServeMux作为处理器,然后通过http.Handle函数将用户自定义的处理器绑定至DefaultServeMux。
package main
// 处理hello的处理器结构
type HelloHandler struct {}
func (h *HelloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w,"Hello!")
}
// 处理world的处理器结构
type WorldHandler struct {}
func (h *WorldHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w,"World!")
}
func main() {
helloHandler := HelloHandler{}
worldHandler := WorldHandler{}
server := http.Server{
Addr: "127.0.0.1:8080",
// 处理器置为空,就会使用默认的多路复用器。
Handler: nil,
}
=========http.Handle函数将用户自定义的处理器绑定至DefaultServeMux=========
http.Handle("/hello",&helloHandler)
http.Handle("/world",&worldHandler)
err := server.ListenAndServe()
if err != nil {
return
}
}
http.Handle函数如何将用户自定义的处理器绑定到多路复用处理器上呢?看一下http.Handle的源码:
// Handle registers the handler for the given pattern in the DefaultServeMux.
// The documentation for ServeMux explains how patterns are matched.
func Handle(pattern string, handler Handler) { DefaultServeMux.Handle(pattern, handler) }
根据源码可知,调用http.Handle函数其实就是调用DefaultServeMux.Handle(pattern, handler)函数,之所以这样做,就是为了操作便利。
另外,虽然Handle函数源于http包,但实际上是ServeMux结构的方法,这也是为了方便。
4.2 处理器函数
处理器函数实际上就是与处理器拥有相同行为的函数:这些函数与ServeHTTP方法拥有相同的签名,即处理器函数接收ResponseWriter和指向Request结构的指针作为参数。只不过将处理器函数绑定到多路复用器上时,使用的是http.HandleFunc()方法。
=== hello函数的类型就是HandlerFunc类型 =====
func hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w,"Hello!")
}
=== world函数的类型就是HandlerFunc类型 =====
func world(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w,"World!")
}
func main() {
server := http.Server{
Addr: "127.0.0.1:8080",
Handler: nil,
}
http.HandleFunc("/hello", hello)
http.HandleFunc("/world", world)
err := server.ListenAndServe()
if err != nil {
return
}
}
处理器和处理器函数的异同
HandleFunc函数会将hello处理器函数转换为一个Handler,并将它与DefaultServeMux进行绑定,以此来简化创建并绑定Handler的工作。
也就是说处理器函数只不过是创建处理器的一种简便的方法而已。
但是,处理器函数并不能完全替代处理器。因为在某些情况下,代码可能已经包含了某个接口或某种类型,这是我们只需要为它们添加ServeHTTP方法就可以将它们转变为处理器,并且这种转变也有助于构建出更为模块化的Web应用。
4.3 串联多个处理器和处理器函数
【案例】假设我们想要在每个处理器被调用时,在某个地方记录下相应的调用信息。
方式一:在处理器中添加一些额外的代码;
方式二:将这些记录日志的代码重构成一个工具函数,然后让每个处理器都去调用这个工具函数。
反思:
虽然实现上述两种方式并不难,但是引入额外代码的做法会给程序的编写带来麻烦,并导致处理器需要包含于处理请求无关的代码。
横切关注点:例如日志记录、安全检查和错误处理这样的操作都被称为横切关注点。
为了防止代码重复和代码依赖的问题,不希望这些操作和正常代码搅合在一起,为此可以使用串联技术分隔代码中的横切关注点。
4.3.1 串联处理器函数
func hello(w http.ResponseWriter, r *http.Request) {
_, _ = fmt.Fprintf(w, "Hello!")
}
func log(h http.HandlerFunc) http.HandlerFunc{
return func(w http.ResponseWriter, r *http.Request) {
name := runtime.FuncForPC(reflect.ValueOf(h).Pointer()).Name()
fmt.Println("Handler function called - " + name)
h(w,r)
}
}
func protect(h http.HandlerFunc) http.HandlerFunc{
return func(w http.ResponseWriter, r *http.Request) {
name := runtime.FuncForPC(reflect.ValueOf(h).Pointer()).Name()
fmt.Println("Handler function called - " + name)
h(w,r)
}
}
func main() {
fmt.Printf("%T",hello)
server := http.Server{
Addr: "127.0.0.1:8080",
Handler: nil, // 为空表示使用多路复用器进行请求分发
}
http.HandleFunc("/hello", log(hello))
// 串联多个处理器函数
http.HandleFunc("/world",protect(log(hello)))
err := server.ListenAndServe()
if err != nil {
return
}
}
4.3.2 串联处理器
func hello(w http.ResponseWriter, r *http.Request) {
_, _ = fmt.Fprintf(w, "Hello!")
}
func log(h http.HandlerFunc) http.HandlerFunc{
return func(w http.ResponseWriter, r *http.Request) {
name := runtime.FuncForPC(reflect.ValueOf(h).Pointer()).Name()
fmt.Println("Handler function called - " + name)
h(w,r)
}
}
func protect(h http.HandlerFunc) http.HandlerFunc{
return func(w http.ResponseWriter, r *http.Request) {
name := runtime.FuncForPC(reflect.ValueOf(h).Pointer()).Name()
fmt.Println("Handler function called - " + name)
h(w,r)
}
}
func main() {
fmt.Printf("%T",hello)
server := http.Server{
Addr: "127.0.0.1:8080",
Handler: nil, // 为空表示使用多路复用器进行请求分发
}
http.HandleFunc("/hello", log(hello))
http.HandleFunc("/world",protect(log(hello)))
err := server.ListenAndServe()
if err != nil {
return
}
}
串联处理器和处理器函数是一种非常常见的惯用法,很多Web应用框架都使用这一技术。