功能说明
示例定义 Middleware 类型与 chain 函数,按传入顺序包裹 http.Handler。requestID 读取或生成 X-Request-ID,写入响应头并放进 context;logging 用 statusRecorder 捕获状态码和字节数;recovery 捕获 handler 的 panic 并返回 500。main 使用 httptest 依次调用正常、401 和 panic 三个端点,验证响应与请求 ID。
关键代码
chain 从切片末尾反向包装,因此 chain(mux, requestID, logging, recovery) 的执行顺序是 requestID、logging、recovery、业务 handler。recovery 位于 logging 内层,panic 被转换为 500 后,外层 statusRecorder 才能记录正确状态。statusRecorder 在首次 Write 时补记 200,并在 WriteHeader 中保存状态码。requestID 通过自定义 contextKey 取值,避免字符串键冲突。
使用注意
包装 ResponseWriter 会丢失 Flusher、Hijacker 等可选接口,流式响应或 WebSocket 场景需要显式转发。恢复中间件只能在尚未写出响应时安全调用 http.Error;若 handler 已写入部分内容,客户端可能收到截断响应。请求 ID 来自客户端时应校验长度与字符集,避免日志注入。生产环境还需加超时、限流和真实客户端 IP 处理。
验证方式
运行 main,/ok 返回 201 且带 X-Request-ID,/hello 返回 401,/boom 触发 panic 日志并返回 500。检查日志中三个请求的 status、bytes、duration 和 request_id 字段是否齐全。可在测试中传入固定请求头断言 ID 透传,并故意让 handler panic 后确认响应码为 500 且进程未退出。
代码示例
package main
import (
"context"
"fmt"
"log"
"net/http"
"net/http/httptest"
"time"
)
type Middleware func(http.Handler) http.Handler
func chain(handler http.Handler, middlewares ...Middleware) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
handler = middlewares[i](handler)
}
return handler
}
type contextKey string
const requestIDKey contextKey = "request_id"
func requestID(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 = fmt.Sprintf("req-%d", time.Now().UnixNano())
}
w.Header().Set("X-Request-ID", id)
ctx := context.WithValue(r.Context(), requestIDKey, id)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
type statusRecorder struct {
http.ResponseWriter
status int
bytes int
}
func (s *statusRecorder) WriteHeader(code int) {
s.status = code
s.ResponseWriter.WriteHeader(code)
}
func (s *statusRecorder) Write(body []byte) (int, error) {
if s.status == 0 {
s.status = http.StatusOK
}
n, err := s.ResponseWriter.Write(body)
s.bytes += n
return n, err
}
func logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
recorder := &statusRecorder{ResponseWriter: w}
next.ServeHTTP(recorder, r)
if recorder.status == 0 {
recorder.status = http.StatusOK
}
id, _ := r.Context().Value(requestIDKey).(string)
log.Printf("method=%s path=%s status=%d bytes=%d duration=%s request_id=%s",
r.Method, r.URL.Path, recorder.status, recorder.bytes,
time.Since(start).Round(time.Millisecond), id)
})
}
func recovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if value := recover(); value != nil {
log.Printf("panic recovered: %v", value)
http.Error(w, "internal server error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/ok", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusCreated)
fmt.Fprintln(w, "created")
})
mux.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "token missing", http.StatusUnauthorized)
})
mux.HandleFunc("/boom", func(w http.ResponseWriter, r *http.Request) {
panic("unexpected nil map")
})
handler := chain(mux, requestID, logging, recovery)
for _, path := range []string{"/ok", "/hello", "/boom"} {
req := httptest.NewRequest(http.MethodGet, path, nil)
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, req)
fmt.Printf("%s -> %d request_id=%s\n", path, recorder.Code, recorder.Header().Get("X-Request-ID"))
}
}