要学习的是如何使用 Go 语言去创建一个 Web 服务器,以及如何处理客 户端发送的请求。

前面一章展示了一个Web编程的Demo,本章着重处理细节。

1. 使用Go构建服务器

GoWeb编程这本书里提到了一个非常有趣的观点:货物崇拜编程(cargo cult programming)

货物崇拜一词指二战期间太平洋某些原住民社会中产生的信仰。他们会建造模仿飞机形状的模型和形似飞机跑道的设施,期待能够召唤飞机,像战争时代一样给他们带来物资。

这个名词常指不熟练的或没经验的程序员从某处拷贝代码到另一处,却並未了解其代码是如何工作的,或者不清楚在新的地方是否需要这段代码。

所以在goweb编程中,不应该局限于成熟框架的使用,而是应该深入理解作用机理,避免货物崇拜。


net/http标准库构成如下:


go构建的服务器处理请求的过程是:

创建服务器的函数时http.ListenAndServe("网络地址",nil)。这个函数传入的网络地址如果为空,则默认使用80端口进行连接;如果处理器参数为nil,则服务器将使用默认的多路复用器DefaultServeMux

最简单的Web服务器如下:

import "net/http"

func main(){
    http.ListenAndServe("",nil)
}

在浏览器端口输入localhost:80,会进入一个页面,显示404,表明服务器创建成功,但是还没有添加页面。

用户还可以通过 Server 结构对服务器进行更详细的配置,包括为请求读取操作设置超时时间等。

func main(){
    server:=http.Server{
        Addr: "127.0.0.1:8080",
        Handler: nil,
    }
    server.ListenAndServe()
}

2. 处理器和处理函数

之前的代码中,我们启动了一个 Web 服务器,访问这个服务器只会获得一个 404HTTP 响应代码。出现这一问题的原因在于我们尚未为服务器编写任何处理器,所以服务器的多路复用器在接收到请求之后找不到任何处理器来处理请求,因此它只能返回一个 404 响应。

2.1 处理请求

在 Go 语言中, 一个处理器就是一个拥有 ServeHTTP 方法的接口,这个函数接收两个参数:

  • ResponseWriter接口
  • 指向Request结构的指针
ServeHTTP(http.ResponseWriter, *http.Request)

在之前ListenAndServe中,输入nil默认采用多路复用器DefaultServeMux。这个复用器是ServeMux结构的实例,它也拥有ServeMux结构的实例,也是Handler处理器结构的实例。

我们可以实现一个自己的处理器:

type MyHandler struct{}

func (h *MyHandler) ServeHTTP(w http.ResponseWriter,r *http.Request){
    fmt.Fprintf(w,"hello")
}
func main(){
    server:=http.Server{
        Addr: "127.0.0.1:8080",
        Handler: &MyHandler{},
    }
    server.ListenAndServe()
}

解释一下,func后面的括号表示此函数属于括号中的结构。比如,

type Mutatable struct {
    a int
    b int
}

func (m Mutatable) StayTheSame() {
    m.a = 5
    m.b = 7
}

这个处理器的问题是:当我们输入localhost:8080/xxsak之类的其他网页URL时,依然弹出hello world,正常来说应该弹出404才对。这是因为:处理器代替原本正在使用的默认多路复用器。 这意味着服务器不会再通过 URL 匹配来将请求路由至不同的处理器,而是直接使用同一个处理器来处理所有请求,因此无论浏览器访问什么地址,服务器返回的都是同样的 Hello World 响应。

2.2 使用多个处理器

为了避免上述的问题,我们不再在 Server 结构的 Handler 字段中指定处理器,而是让服务器使用默认的 DefaultServeMux 作为处理器, 然后通过 http.Handle 函数将处理器绑定至 DefaultServeMux

通过下面的程序我们能实现:

  • 输入localhost:8080/hello,弹出hello
  • 输入localhost:8080/world,弹出world
  • 输入其他地址,弹出404
type hellohander struct{}
func (h *hellohander) ServeHTTP(w http.ResponseWriter,r *http.Request){
    fmt.Fprintf(w,"hello")
}

type worldhander struct{}
func (h *worldhander) ServeHTTP(w http.ResponseWriter,r *http.Request){
    fmt.Fprintf(w,"world")
}
func main(){
    server:=http.Server{
        Addr: "127.0.0.1:8080",
    }
    http.Handle("/hello",&hellohander{})
    http.Handle("/world",&worldhander{})
    server.ListenAndServe()
}

2.3 处理器函数

http的处理器函数相当于一个转换器:将一个带有正确签名的函数f转换成一个带有方法f的Handler,比如:

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

http.HandleFunc("/world",world)

2.4 串联多个处理器和处理器函数

前面介绍了,在 Go 语言里面,程序可以将一个 函数传递给另一个函数,又或者通过标识符去引用一个具名函数。这意味着,程序可以像图展示的那样,将函数 fl 传递给另一个函数口,然后在函数 f2 执行完某些操作之后调用 fl 。

平时有些工作,诸如如日志记录、安全检查和错误处理等,需要我们加到正常代码中处理,这被称为横切关注点。但我们又不希望这些操作和正常的代码搅和在一起。 为此,我们可以使用串联( chaining )技术分隔代码中的横切关注点。

下面这段代码,log函数接受一个HandlerFunc函数,然后又返回一个HandlerFunc函数,通过反射机制,反射出调用log的函数是谁。此时log函数相当于一个检验器

当用户进入localhost:8080/world时,控制台输出Handler function called -main.world

func world(w http.ResponseWriter,r *http.Request){
    fmt.Fprintf(w,"world")
}
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 main(){
    server:=http.Server{
        Addr: "127.0.0.1:8080",
    }
    http.Handle("/world",log(world))
    server.ListenAndServe()
}

当然我们也可以串联起更多函数。 串联多个函数可以让程序执行更多动作,这种做法有时候也称为管道处理( pipeline processing)

比如,我们写一个验证用于身份的函数:

func protect(h http.HandlerFunc) http.HandlerFunc{
    return func(w http.ResponseWriter, r *http.Request){
        ... //保护
        h(w,r)
    }
}
http.HandleFunc("/hello",protect(log(hello)))

串联处理器的方法实际上和串联处理器 函数的方法是非常相似的。

type HelloHandler struct{}

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

func log(h http.Handler) http.Handler{
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request){
        fmt.Printf("Handler called-%T\n",h)
        h.ServeHTTP(w,r)
    })
}
hello : = HelloHandler{} 
http.Handle ("/hello", log (hello))

传入log一个handler,在里面直接经过HandlerFunc函数处理得到一个Handler,这样就可以直接在http.Handle内执行。注意http.HandleFunchttp.HandlerFunc目测可以直接混用。

3.5 ServeMux和DefaultServeMux

前面介绍了多路复用器,它负责接收HTTP请求并根据请求中的统一资源定位符URL将请求重新定向到正确的处理器

ServeMux包含了一个map,通过map将URL映射到处理器,正如之前所说,ServeMux结构也实现了ServeHTTP方法,他也是一个处理器:

DefaultServeMuxServeMux的一个实例,当用户没有为 Server 结构指定处理器时,服务器就会使用 DefaultServeMux 作为 ServeMux 的默认实例。


当匹配不成功时,比如输入/lalalal会进行层级下降,最终落到根URL上,也就是indexHanlder,输出404。但是当我们输入/world/123123,并不会落在/world上,这是因为程序在绑定 helloHandler 时使用的 URL 是/hello 而不是 /hello/如果被绑定的 URL 不是以/结尾,那么它只会与完全相同的 URL 匹配