Go中间件必须返回http.Handler,正确签名是func(http.Handler) http.Handler;错误写法如func(w,r)或无返回值会导致注册失败或请求卡死,且必须显式调用next.ServeHTTP(w, r)才能放行请求。

必须返回 http.Handler 才算中间件
很多人写了个 func(w http.ResponseWriter, r *http.Request) 就以为是中间件,结果注册时报错或压根不执行。Go 的 HTTP 服务只认实现了 http.Handler 接口的类型(即有 ServeHTTP(http.ResponseWriter, *http.Request) 方法),或者能转成 http.HandlerFunc 的函数。
正确签名只有一种:func(next http.Handler) http.Handler。外层接收配置(比如日志器、开关标志),中层符合这个签名,内层用 http.HandlerFunc 包裹逻辑。
- 错误写法:
func logging() { ... }或func logging(w, r) { ... }—— 无法被http.Handle、http.ListenAndServe或 Gin/Echo 等框架识别 - 正确结构:必须调用
next.ServeHTTP(w, r),否则请求卡死;调用位置决定是前置、后置还是拦截逻辑 - 如果要用
context.Context传值(比如用户 ID),得用r.WithContext(newCtx)再传给next.ServeHTTP,原r不会自动更新
r.RequestURI 比 r.URL.Path 更可靠
反向代理、URL 重写、Path 覆盖等场景下,r.URL.Path 经常被标准库解析篡改,比如变成 /,而真实路径藏在原始请求行里。r.RequestURI 是服务端收到的未解析字符串(如 /api/v1/users?id=123),适合日志归档。
- 注意它带查询参数,敏感字段(如
token=、password=)需正则脱敏,否则泄露风险高 - 别用
r.URL.String():会补?、漏 fragment、编码不一致,日志对不上请求 - 若用了
gorilla/mux,可用mux.CurrentRoute(r).GetName()补充路由名,但要包recover,避免匹配失败 panic
状态码和耗时必须靠包装 ResponseWriter
标准 http.ResponseWriter 不暴露已写的状态码,w.WriteHeader(status) 调用后也不会通知你。想记录真实状态码,必须自己实现一个包装器,重写 WriteHeader 和 Write 方法。
立即学习“go语言免费学习笔记(深入)”;
- 漏掉
Write方法会导致响应体没被捕获,部分框架(如 fasthttp 兼容层)可能绕过WriteHeader直接写,所以两个方法都要覆盖 - 耗时计算必须用
time.Now().UnixMicro(),别用time.Since(start).String()——字符串格式化开销大,高频请求下可观测性数据本身就成了性能瓶颈 - 别在 wrapper 里做 JSON 序列化,先存结构体字段,最后统一 encode;否则每次写都触发 GC
JSON 日志里的 Unicode 转义怎么关
标准 json.Marshal 默认把中文、emoji 全转成 \u4f60\u597d,查日志像解码,ES/Kibana 也难做 term 查询。这不是 bug,是安全默认,但生产日志真没必要。
- 用
json.Encoder替代json.Marshal,调enc.SetEscapeHTML(false)可禁 HTML 字符转义(<→\u003c),但不影响 Unicode - 彻底禁 Unicode 转义:要么用第三方库如
github.com/goccy/go-json,要么自定义结构体的MarshalJSON方法,手动拼接(注意引号和逗号) - 千万别给数字字段加
json:",string"标签,会导致"status": "200",ES 自动映射成 keyword,无法 range 查询
真正难的不是写完,而是让日志在高并发、代理链、Body 多次读、panic 恢复这些边界下依然稳定输出——尤其是 r.Body 被多个中间件读取时,必须用 io.NopCloser + bytes.NewReader 重放,否则下游 handler 读不到。


















