Go HTTP 中间件入门:日志、恢复、鉴权和请求 ID 怎么串起来

本文讲解 Go HTTP 中间件的基本模式,使用标准库实现日志、panic 恢复、请求 ID 和简单鉴权链路。

中间件本质上是在包装 Handler

Go 的 HTTP handler 是:

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

中间件就是接收一个 handler,返回一个新的 handler。新 handler 可以在调用原 handler 前后做事情,比如记录日志、恢复 panic、检查登录状态、加请求 ID、统计耗时、设置响应头。

理解这个模式后,你会发现很多 Web 框架的中间件都不神秘。标准库也能写出清楚的中间件链。

中间件类型

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
}

使用:

handler := Chain(mux,
	Recover(logger),
	RequestID(),
	RequestLogger(logger),
)

顺序很重要。上面的顺序表示请求先经过 Recover,再经过 RequestID,再经过 RequestLogger,最后到 mux。响应返回时顺序反过来。

请求 ID 中间件

type contextKey string

const requestIDKey contextKey = "request_id"

func RequestID() Middleware {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			id := r.Header.Get("X-Request-ID")
			if id == "" {
				id = newRequestID()
			}

			ctx := context.WithValue(r.Context(), requestIDKey, id)
			w.Header().Set("X-Request-ID", id)
			next.ServeHTTP(w, r.WithContext(ctx))
		})
	}
}

func RequestIDFromContext(ctx context.Context) string {
	id, _ := ctx.Value(requestIDKey).(string)
	return id
}

生成 ID:

func newRequestID() string {
	b := make([]byte, 16)
	if _, err := rand.Read(b); err != nil {
		return fmt.Sprintf("%d", time.Now().UnixNano())
	}
	return hex.EncodeToString(b)
}

请求 ID 让日志和排查更方便。context value 要克制使用,适合请求范围元信息,不适合传业务参数。

panic 恢复

func Recover(logger *slog.Logger) Middleware {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			defer func() {
				if value := recover(); value != nil {
					logger.Error("panic recovered",
						"panic", value,
						"request_id", RequestIDFromContext(r.Context()),
					)
					http.Error(w, "internal server error", http.StatusInternalServerError)
				}
			}()

			next.ServeHTTP(w, r)
		})
	}
}

恢复 panic 可以避免单个请求把整个服务打崩。但这不是错误处理替代品。可预期失败仍然应该返回 error 并转成 HTTP 响应。panic 更适合真正不可恢复的编程错误。

简单鉴权

func RequireToken(expected string) Middleware {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
			if token == "" || token != expected {
				http.Error(w, "unauthorized", http.StatusUnauthorized)
				return
			}
			next.ServeHTTP(w, r)
		})
	}
}

这只是入门示例。真实系统要考虑 token 签名、过期、权限范围、密钥轮换和审计日志。但中间件位置是合适的:鉴权通过后才进入业务 handler。

请求日志

func RequestLogger(logger *slog.Logger) Middleware {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			start := time.Now()
			next.ServeHTTP(w, r)
			logger.Info("http request",
				"request_id", RequestIDFromContext(r.Context()),
				"method", r.Method,
				"path", r.URL.Path,
				"duration_ms", time.Since(start).Milliseconds(),
			)
		})
	}
}

更完整的版本会记录状态码,需要包装 ResponseWriter。入门阶段先把模式掌握清楚。

小结

Go HTTP 中间件就是 func(http.Handler) http.Handler。通过包装 handler,可以把日志、请求 ID、panic 恢复、鉴权、限流、压缩等横切逻辑从业务 handler 中拿出去。

中间件顺序要明确,context value 要克制,panic 恢复不能替代正常错误处理。用标准库写一次中间件链,你会更理解任何 Go Web 框架的工作方式。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页