2、HTTP基础回顾:Go标准库net/http、Handler与HandlerFunc、ServeMux路由
说实话,很多同学一上来就追着问「中间件怎么写」,结果连最基础的 HTTP 处理都没搞明白。我见过太多人把中间件写成了黑魔法,其实底层就是几个接口和函数的事。
这一节,咱们把 Go 标准库 net/http 的核心概念彻底捋一遍。你想想看,中间件本质上就是一层层包装的 Handler,如果连 Handler 是什么都没吃透,后面写中间件肯定要踩坑。
2.1 net/http 的请求处理模型
Go 的 HTTP 服务,说白了就是一个循环:
- 监听端口,等待连接
- 收到请求,解析 HTTP 报文
- 找到对应的处理器,调用它
- 处理器写回响应
这个模型从 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.Method 和 r.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.HandleFunc 和 http.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/mux、chi、gin 这些第三方路由。
但理解默认 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,会自动写入 200WriteHeader():写入状态码。一旦调用,就不能再修改响应头了
这里有个顺序问题:必须先设置 Header,再调用 WriteHeader,最后 Write。我见过有人先 Write 再设置 Header,结果 Header 全丢了。
小技巧:如果你要返回 JSON,记得先设置 w.Header().Set("Content-Type", "application/json")。我习惯在写中间件时统一处理这个,省得每个 Handler 都写一遍。
2.7 小结
这一节的内容,说白了就是三个概念:
- Handler 接口:一切 HTTP 处理的基石
- HandlerFunc:把函数变成 Handler 的适配器
- ServeMux:根据路径分发请求的路由器
下一节,我们会基于这些基础,开始写第一个中间件。你想想看,中间件不就是「在 Handler 外面再包一层 Handler」吗?理解了这一点,后面就顺了。