HTTP 中间件是 Web 服务里非常常见的设计。日志、鉴权、跨域、压缩、panic 恢复、限流、请求追踪都可以通过中间件实现。理解中间件链之后,即使不使用大型 Web 框架,也能基于标准库写出清晰的服务结构。

本文使用 Go 标准库的 net/http 实现一个简单中间件链,并说明执行顺序、错误处理和扩展方式。

中间件的本质

在标准库里,一个 HTTP 处理器只要实现 http.Handler 接口即可:

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

中间件可以理解为一个函数:接收一个 http.Handler,返回一个新的 http.Handler

1
type Middleware func(http.Handler) http.Handler

这样就可以在真正的业务 handler 前后增加逻辑。

编写日志中间件

先实现一个简单的访问日志中间件:

1
2
3
4
5
6
7
8
9
func Logger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()

next.ServeHTTP(w, r)

log.Printf("%s %s cost=%s", r.Method, r.URL.Path, time.Since(start))
})
}

这里的关键是 next.ServeHTTP(w, r)。它代表调用链继续向后执行。调用前可以记录开始时间,调用后可以记录耗时。

panic 恢复中间件

服务端代码难免存在未预料的 panic。中间件可以统一 recover,避免整个进程退出:

1
2
3
4
5
6
7
8
9
10
11
12
func Recover(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if v := recover(); v != nil {
log.Printf("panic path=%s err=%v", r.URL.Path, v)
http.Error(w, "internal server error", http.StatusInternalServerError)
}
}()

next.ServeHTTP(w, r)
})
}

这个中间件应该放在比较外层,这样后续中间件和业务 handler 发生 panic 时都能被捕获。

鉴权中间件

再写一个简单的 token 鉴权中间件:

1
2
3
4
5
6
7
8
9
10
11
func Auth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token != "Bearer demo-token" {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}

next.ServeHTTP(w, r)
})
}

如果鉴权失败,不调用 next.ServeHTTP,请求就在当前中间件终止。这个特性适合鉴权、限流、参数校验等前置逻辑。

构建中间件链

可以写一个 Chain 函数把多个中间件组合起来:

1
2
3
4
5
6
func Chain(h http.Handler, middlewares ...Middleware) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
h = middlewares[i](h)
}
return h
}

注意这里从后往前包裹。假设调用:

1
handler := Chain(finalHandler, Recover, Logger, Auth)

实际执行顺序是:

1
Recover -> Logger -> Auth -> finalHandler -> Logger -> Recover

外层中间件先进入,内层中间件先返回。

完整示例

下面是一个可以直接运行的小服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package main

import (
"log"
"net/http"
"time"
)

type Middleware func(http.Handler) http.Handler

func Chain(h http.Handler, middlewares ...Middleware) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
h = middlewares[i](h)
}
return h
}

func main() {
mux := http.NewServeMux()

mux.Handle("/hello", Chain(
http.HandlerFunc(hello),
Recover,
Logger,
Auth,
))

log.Println("server listen on :8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}

func hello(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("hello"))
}

func Logger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s cost=%s", r.Method, r.URL.Path, time.Since(start))
})
}

func Recover(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if v := recover(); v != nil {
log.Printf("panic: %v", v)
http.Error(w, "internal server error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}

func Auth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") != "Bearer demo-token" {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}

测试请求:

1
2
curl -i http://localhost:8080/hello
curl -i -H 'Authorization: Bearer demo-token' http://localhost:8080/hello

第一个请求会返回 401,第二个请求返回 hello

记录状态码

上面的日志中间件没有记录响应状态码,因为标准库的 ResponseWriter 不会直接暴露状态。可以包装它:

1
2
3
4
5
6
7
8
9
type statusWriter struct {
http.ResponseWriter
status int
}

func (w *statusWriter) WriteHeader(status int) {
w.status = status
w.ResponseWriter.WriteHeader(status)
}

在日志中间件中使用:

1
2
3
sw := &statusWriter{ResponseWriter: w, status: http.StatusOK}
next.ServeHTTP(sw, r)
log.Printf("status=%d", sw.status)

这样就能记录每个请求的最终状态码。

小结

Go 的 HTTP 中间件并不依赖框架。只要理解 func(http.Handler) http.Handler 这个签名,就可以把通用逻辑从业务 handler 中拆出来。实践中建议把 recover、日志、trace id 放在外层,鉴权、限流、参数校验放在业务入口前,保持链路清晰。