Go 语言系列之 HTTP 服务器

Go 语言系列之 HTTP 服务器

初学者的疑问

让我们开门见山吧

package main
import (
    "fmt"
    "net/http"
// 自定义两个handler
func hello(w http.ResponseWriter, req *http.Request) {
    fmt.Fprintf(w, "hello\n")
func headers(w http.ResponseWriter, req *http.Request) {
    for name, headers := range req.Header {
        for _, h := range headers {
            fmt.Fprintf(w, "%v: %v\n", name, h)
func main() {
    // 将上述两个handler注册到对应的路由地址
    http.HandleFunc("/hello", hello)
    http.HandleFunc("/headers", headers)
    http.ListenAndServe(":8090", nil)

代码逻辑很简单,运行之后的结果就是开启了一个端口为 8090 HTTP 服务。

初次见到类似的代码,我最大的疑问就是 ListenAndServe 第二个参数为啥是 nil 啊!

ListenAndServe 第二个参数需要一个实现 Handler 接口的实例。

// net/http/server.go
func ListenAndServe(addr string, handler Handler) error
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)

但是在上面的代码中,居然传递了 nil ,更神奇的是它居然能够生效。

我们使用 curl 访问之,发现 hello headers 两个 handler 已经注册到 HTTP 服务器了。

✘    curl -v http://127.0.0.1:8080/hello
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> GET /hello HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.54.0
> Accept: */*
< HTTP/1.1 200 OK
< Date: Thu, 27 Oct 2022 06:54:09 GMT
< Content-Length: 12
< Content-Type: text/plain; charset=utf-8
* Connection #0 to host 127.0.0.1 left intact
hello world.%

为什么能够生效?

答案当然是很简单的。 代码的世界没有神奇一说,更没有魔法 ,一定是 net/http 具体的实现帮助我们在某处进行了”连接“。

与其单纯的探究这个问题,我们不妨提出一个更好的问题: Go 语言的 HTTP 服务器的生命周期是什么样的。

具体来说,当一个 request 请求进入, HTTP 服务器是如何将之交给与对应的 handler 处理,如何进行的匹配?如何返回的 response

HTTP 请求的生命周期

经过对代码的深入阅读,整理一下 HTTP 请求如何与 HTTP 服务器交互。


image.png


  • http.ListenAndServe 首先在某端口上开始监听
  • HTTP client 发出 HTTP 请求之后,需要与 HTTP server 建立连接,即建立两端的 tcp 连接
  • HTTP server 开启一个 goroutine ,专门处理这个连接上的请求、响应:

1、在 goroutine 内部解析出 request

2、根据请求路径匹配对应的 handler

3、调用 handler ,处理这个请求,返回 response

解决我的疑问

之前我提到的自定义 handler 是如何神奇的绑定到 HTTP server 上。根据上一节的「 HTTP 请求的生命周期」,现在我们很容易就能定位将 HTTP server 和自定义 handler 串通起来的连接点 —— 读取 req 之后的 ServeHTTP 方法。

重点就是这个『调用 handler 』的步骤。

// net/http/server.go
// 找到咯,就是在这里
// 这里就是调用handler的入口
serverHandler{c.server}.ServeHTTP(w, w.req)

下面进入这个调用的内部实现

type serverHandler struct {
    srv *Server
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
    handler := sh.srv.Handler
    // 神奇的源头在这里。
    if handler == nil {
        handler = DefaultServeMux
    if req.RequestURI == "*" && req.Method == "OPTIONS" {
        handler = globalOptionsHandler{}
    handler.ServeHTTP(rw, req)

啊哈!

因为我们调用 http.ListenAndServe(":8090", nil) 时第二个参数为 nil ,所以 HTTP 服务器默认启用 DefaultServeMux 作为自己的 handler

if handler == nil {
    handler = DefaultServeMux

而我们自定义的 hello/headers 两个 handler 就是注册在 DefaultServeMux 上的。我们有代码作证:

// 将hello/headers 绑定
http.HandleFunc("/hello", hello)
http.HandleFunc("/headers", headers)
// http.HandleFunc的实现
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    DefaultServeMux.HandleFunc(pattern, handler)

这样一切就明了啊,优咔哒,优咔哒!

不过这个是何物?

var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux

明白了,就是一个零值 ServeMux 的指针类型。下面我们就来讨论一下这个 ServeMux

说到底,ServeMux 是个啥啊?

所以这篇文章关 ServeMux 什么关系?到底是如何引入的 ServeMux 来着?

不要急,是这样的。让我们回顾一下。

// main主函数里面如此调用http.HandleFunc,注册hello自定义handler
http.HandleFunc("/hello", hello)
// http.HandleFunc本质是将传入的handler绑定到DefaultServeMux
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    DefaultServeMux.HandleFunc(pattern, handler)
var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux

1、 main 主函数里面如此调用 http.HandleFunc ,注册 hello 自定义 handler

2、 http.HandleFunc 本质是将传入的 handler 绑定到 DefaultServeMux

3、 DefaultServeMux = &ServeMux{}

没错, DefaultServeMux 就是 *ServeMux

当我们调用 http.ListenAndServe(":8080", nil) 启动 HTTP 服务器,且第二个参数为 nil 时,实际上就是把 DefaultServeMux 作为第二个参数传递进去了啊。

好吧好吧,你说了这么多,不是第一节就已经说明的事情嘛。干嘛还翻来覆去的说个不停呢?

好,下面进入重点。

http.ListenAndServe 的第二个参数是个啥类型?其实上面已经说过了,是一个实现 http.Handler 接口的实例。

// net/http/server.go
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)

从本质上来说, DefaultServeMux 就是一个 Handler 接口实例而已。

所以为了实现 hello handler 的逻辑,即用户访问 http 服务器,返回用户 hello 字符串信息。

我们完全可以这么写。

type HelloHandler struct {
func (*HelloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, "hello world.")
func main() {
    helloHandler := &HelloHandler{}
    http.ListenAndServe(":8080", helloHandler)

我们自定义了 HelloHandler 类型,其指针类型实现了 Handler 接口,所以直接传入 http.ListenAndServe ,即可发挥作用。此时我们 curl 下,成功返回 “hello world." 字符串。

但是问题有两个:

1、提供一个 handler 处理函数,就需要我们自定义一个实现类型,并且该类型需要实现 ServeHTTP 方法以便满足 Handler 接口。这也太麻烦了吧。

啊,对对对对,说的没错。所以构建 net/http 包的大佬,为我们提供了一个 http.HandlerFunc 类型。

type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)

http.HandlerFunc 本质上是一个自定义类型,实现了 ServeHTTP 方法。

如此一来通过强制转换 http.HandlerFunc(hello) ,就可以将类似 hello 这样平平无奇的函数,变成满足 http.Handler 接口的实例。 HandlerFunc(hello) 类似整数类型的强制转换,比如 int32(666) 。当调用 ServeHTTP 的时候,其实就是调用了 hello 本身嘛。

我们改写代码如下:

func hello(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, "hello world.")
func main() {
    http.ListenAndServe("8080", http.HandlerFunc(hello))

真是精彩!

2、虽然一行代码就启动了 HTTP 服务器。可是这个服务器,好像只有一个 handler 方法?

http.ListenAndServe("8080", http.HandlerFunc(hello))

不论我们:

curl -v http://127.0.0.1:8080/h
curl -v http://127.0.0.1:8080/
curl -v http://127.0.0.1:8080/hello
curl -v http://127.0.0.1:8080/shit

都会一丝不苟的返回 hello world 字符串。

咋搞哒!这能叫 HTTP 服务器?这不就是一个小玩具吗?

现在收敛心神,让我们从头开始。

func main() {
    http.HandleFunc("/hello", hello) // 注册到DefaultServeMux上,A ServeMux is just a Handler
    http.HandleFunc("/headers", headers)
    http.ListenAndServe(":8080", nil)

http.HandleFunc 分明就是把路由注册到了 DefaultServeMux 上!然后给每个 tcp 连接开启的 goroutine 上,通过适配器 serverHandler 会匹配请求的路由,调用相应的 handler

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
    handler := sh.srv.Handler
    if handler == nil {
        handler = DefaultServeMux
    if req.RequestURI == "*" && req.Method == "OPTIONS" {
        handler = globalOptionsHandler{}
    // 调用DefaultServeMux的ServeHTTP方法
    // ServeHTTP方法实现路由匹配机制,匹配之后调用我们自定义的handler处理方法
    handler.ServeHTTP(rw, req)

DefaultServeMux ServeHTTP 逻辑

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) // 根据r的请求路由,返回相应的下一级handler
    h.ServeHTTP(w, r) // 调用我们自定义的handler,比如hello

哦,一切明了,没有魔法。

DefaultServeMux 本质上就是一个实现 http.Handler 接口的 Handler 实例。

DefaultServeMux 里面也注册了很多 Handler ,根据路由选择对应的 Handler

所以我们完全可以将最开始的 HTTP 服务器代码改造如下

func main() {
    //http.HandleFunc("/hello", hello)
    //http.HandleFunc("/headers", headers)
    smx := http.NewServeMux()
    smx.HandleFunc("/hello", hello)
    smx.HandleFunc("/headers", headers)
    http.ListenAndServe(":8090", smx)

我们主动创建了一个 ServeMux ,然后将两个自定义的 handler 绑定,最后调用 ListenAndServe 的时候主动将 ServeMux 作为第二个参数传递。

这样可比传递 nil ,内部使用 DefaultServeMux 的方式更容易让人理解呀。

看到这里,你是否已经晕了?就算晕了也不要紧,在你回看之前。重点记住我下面的论述:

  • Q: 当我说 handler 的时候,我指的是什么?
  • A:handler 就是实现了 http.Handler 接口的实例。
  • Q:handler 重要吗?
  • A :重要。因为在 HTTP 服务器中哪里都是 handler ,到处都是 handler ,我们可以自定义 handler ,标准库中内也有自己实现好的 handler handler HTTP 服务器非常重要的一部分。

好了,建议你在重新看一遍上面的章节。

让我们继续向更多的 handler 前进

上面说到处都是 handler ,下面继续深化这个概念,并且狂野一点,让我们开启链式 handler 模式。

DefaultServeMux 本身是个 handler ,然后上面绑定了我们自定义的 handler (比如 hello headers ),本质上是外层 handler 包裹了内层 handler 。你可能想到了,内层的 handler 可以继续绑定 handler ,一直组合一直组合,搞一个链式调用。

通过不断的组合,我们可以增加额外的处理逻辑,这就是传说中的前置处理器和后置处理器。

DefaultServeMux 就是有自己的前置处理器。

它的前置处理器其实我们已经接触过了:根据路由选择具体的下一级的 Handler

为了复习和验证上面的知识,我们搞一个多条链路组合的 handler

加什么好呢?

搞一个计时日志吧!

干!

package main
import (
    "fmt"
    "net/http"
    "time"
type LogHandler struct {
    handler http.Handler
func (l LogHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    now := time.Now()
    l.handler.ServeHTTP(w, r)
    fmt.Printf("serve this handler cost %v\n", time.Since(now))
// 最内层的handler
// 当目前hello只是一个普通的函数
func helloHandler(w http.ResponseWriter, req *http.Request) {
    fmt.Fprintf(w, "hello\n")
func main() {
    smx := http.NewServeMux()
    logHandler := LogHandler{handler: http.HandlerFunc(helloHandler)}
    smx.Handle("/hello", logHandler)
    http.ListenAndServe(":8000", smx)

当我们发送 HTTP 请求后

curl -v http://127.0.0.1:8000/hello
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8000 (#0)
> GET /hello HTTP/1.1
> Host: 127.0.0.1:8000
> User-Agent: curl/7.64.1
> Accept: */*
< HTTP/1.1 200 OK
< Date: Wed, 30 Nov 2022 13:57:22 GMT
< Content-Length: 6
< Content-Type: text/plain; charset=utf-8
hello
* Connection #0 to host 127.0.0.1 left intact
* Closing connection 0

果然打印了

serve this handler cost 56.305µs

这次套了一层,我们还可以继续套娃呢!

func main() {
    smx := http.NewServeMux()
    timeoutHello := http.TimeoutHandler(http.HandlerFunc(helloHandler), 1*time.Second, "you are time out")
    logHandler := LogHandler{handler: timeoutHello}
    // defaultServerMux(Handler) > LogHandler > Timeout Handler > Hello
    smx.Handle("/hello", logHandler)
    http.ListenAndServe(":8000", smx)

当我们调用时

curl -v http://127.0.0.1:8000/hello
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8000 (#0)
> GET /hello HTTP/1.1
> Host: 127.0.0.1:8000
> User-Agent: curl/7.64.1
> Accept: */*
< HTTP/1.1 503 Service Unavailable
< Date: Wed, 30 Nov 2022 14:02:31 GMT