2、HTTP基础回顾:Go标准库net/http、Handler与HandlerFunc、ServeMux路由

说实话,很多同学一上来就追着问「中间件怎么写」,结果连最基础的 HTTP 处理都没搞明白。我见过太多人把中间件写成了黑魔法,其实底层就是几个接口和函数的事。

这一节,咱们把 Go 标准库 net/http 的核心概念彻底捋一遍。你想想看,中间件本质上就是一层层包装的 Handler,如果连 Handler 是什么都没吃透,后面写中间件肯定要踩坑。

2.1 net/http 的请求处理模型

Go 的 HTTP 服务,说白了就是一个循环:

  1. 监听端口,等待连接
  2. 收到请求,解析 HTTP 报文
  3. 找到对应的处理器,调用它
  4. 处理器写回响应

这个模型从 Go 1.0 到现在基本没变过。我最早用 Go 写 Web 服务是 2013 年,那时候连 context 包都没有,路由全靠手写 if-else。嗯,现在回想起来还挺怀念的。

核心代码就这几行:

package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello, World!")
    })
    http.ListenAndServe(":8080", nil)
}

这段代码你肯定见过。但你真的理解 nil 传进去意味着什么吗?

关键点:第二个参数是 Handler 接口,传 nil 表示使用默认的 DefaultServeMux

2.2 Handler 接口——一切的核心

先看这个接口的定义:

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

就一个方法。简单到令人发指。

但就是这一个方法,撑起了整个 Go Web 生态。任何类型,只要实现了 ServeHTTP,就是一个 Handler。我经常跟团队里的人说:「别把 Handler 想得太玄乎,它就是个能处理请求的东西。」

举个例子:

type MyHandler struct{}

func (h *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello from MyHandler"))
}

func main() {
    handler := &MyHandler{}
    http.ListenAndServe(":8080", handler)
}

你看,这就是一个完整的 HTTP 服务。所有请求都走同一个 Handler。当然,实际项目不会这么干,但原理就是这样。

个人习惯:我写 Handler 时,一定会先检查 r.Methodr.URL.Path。因为默认的 ServeMux 对路径匹配有坑,后面会讲。

2.3 HandlerFunc——函数也是 Handler

如果每次都要定义一个结构体,那也太麻烦了。Go 的设计者当然想到了这一点,于是有了 HandlerFunc

type HandlerFunc func(ResponseWriter, *Request)

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

说白了,HandlerFunc 就是一个适配器。它把一个普通函数包装成了 Handler 接口。这就是 Go 中「函数也是类型」的典型应用。

所以下面两种写法是等价的:

// 写法一:直接传函数
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello"))
})

// 写法二:显式转换
http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello"))
}))

我在项目中遇到过有人纠结用 Handle 还是 HandleFunc。其实没区别,HandleFunc 内部也是调用了 Handle,只是帮你做了类型转换。

注意:http.HandleFunchttp.Handle 都是注册到默认的 DefaultServeMux。如果你自己创建了 ServeMux,记得调用它的方法。

2.4 ServeMux——路由分发器

ServeMux 是 Go 标准库自带的路由器。它的工作很简单:根据请求路径,找到对应的 Handler。

来看它的核心逻辑:

type ServeMux struct {
    mu    sync.RWMutex
    m     map[string]muxEntry
    hosts bool
}

type muxEntry struct {
    h       Handler
    pattern string
}

内部就是一个 map,key 是路径模式,value 是处理器。匹配规则是这样的:

  • 精确匹配优先:/api/users 匹配 /api/users
  • 最长前缀匹配:如果没有精确匹配,就找最长的注册路径前缀
  • 根路径 / 是兜底:所有没匹配到的都会落到 /

举个例子:

mux := http.NewServeMux()
mux.HandleFunc("/", rootHandler)
mux.HandleFunc("/api", apiHandler)
mux.HandleFunc("/api/users", usersHandler)

请求 /api/users/profile 会匹配到 /api/users,因为它是前缀匹配中最长的。请求 /api/orders 会匹配到 /api。请求 /about 会匹配到 /

避坑指南:我曾经在项目里注册了 /api//api 两个路径,结果发现 /api 的请求被重定向到了 /api/。这是因为 ServeMux 对带斜杠结尾的路径有特殊处理——它会自动做 301 重定向。嗯,这个坑我踩过两次才记住。

2.5 自定义 ServeMux——为什么需要它?

默认的 ServeMux 够用吗?说实话,小项目还行。但一旦涉及 RESTful API、路径参数、方法路由,它就力不从心了。

比如你想实现这样的路由:

GET    /api/users/{id}
POST   /api/users
DELETE /api/users/{id}

默认 ServeMux 做不到。它不支持路径参数,也不区分 HTTP 方法。所以大家才会用 gorilla/muxchigin 这些第三方路由。

但理解默认 ServeMux 仍然很重要。因为所有第三方路由的底层,最终都是实现了 Handler 接口。你写中间件时,操作的就是这个接口。

来看一个自定义 ServeMux 的简单实现:

type MyMux struct {
    handlers map[string]http.Handler
}

func (m *MyMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    key := r.Method + ":" + r.URL.Path
    if handler, ok := m.handlers[key]; ok {
        handler.ServeHTTP(w, r)
        return
    }
    http.NotFound(w, r)
}

你看,核心还是 ServeHTTP 方法。这就是为什么我说「理解了 Handler 接口,就理解了 Go Web 的一半」。

2.6 ResponseWriter——写回响应的接口

最后聊聊 ResponseWriter。它是一个接口,不是结构体:

type ResponseWriter interface {
    Header() Header
    Write([]byte) (int, error)
    WriteHeader(statusCode int)
}

三个方法,各司其职:

  • Header():获取响应头,可以设置 Content-Type、Set-Cookie 等
  • Write():写入响应体。如果没调用过 WriteHeader,会自动写入 200
  • WriteHeader():写入状态码。一旦调用,就不能再修改响应头了

这里有个顺序问题:必须先设置 Header,再调用 WriteHeader,最后 Write。我见过有人先 Write 再设置 Header,结果 Header 全丢了。

小技巧:如果你要返回 JSON,记得先设置 w.Header().Set("Content-Type", "application/json")。我习惯在写中间件时统一处理这个,省得每个 Handler 都写一遍。

2.7 小结

这一节的内容,说白了就是三个概念:

  • Handler 接口:一切 HTTP 处理的基石
  • HandlerFunc:把函数变成 Handler 的适配器
  • ServeMux:根据路径分发请求的路由器

下一节,我们会基于这些基础,开始写第一个中间件。你想想看,中间件不就是「在 Handler 外面再包一层 Handler」吗?理解了这一点,后面就顺了。