log.Println 拿不到原始请求头是因为中间件顺序和 Header 延迟解析导致;应在最外层中间件中用 r.Header.Clone() 快照头,并脱敏敏感字段、按需格式化打印。

为什么 log.Println 拿不到请求头?
因为 Go 的 http.Request 默认不把原始请求头全部暴露给日志中间件——你直接打印 r.Header 看起来有内容,但实际是 Header 类型的 map,底层做了大小写归一化和部分字段延迟解析。更关键的是,如果中间件在 http.Handler 链中位置靠后(比如套了 CORS、gzip 中间件),某些头可能已被修改或删除。
真正可靠的方式是:在中间件最开始、next.ServeHTTP 之前读取并快照 r.Header,且注意处理多值头(如 Cookie、Accept-Encoding)。
用 r.Header.Clone() 安全复制请求头
Go 1.21+ 原生支持 r.Header.Clone(),它返回一个深拷贝的 http.Header,避免后续中间件篡改影响日志。低于 1.21 则需手动遍历复制:
// Go 1.21+
headers := r.Header.Clone()
// Go < 1.21
headers := make(http.Header)
for k, vv := range r.Header {
headers[k] = append([]string(nil), vv...)
}
- 别用
map[string][]string(r.Header)直接转,会共享底层 slice,后续修改会导致日志错乱 -
User-Agent、Referer、X-Forwarded-For这类业务常用头建议单独提取,避免被过滤或混淆 - 敏感头(如
Authorization、Cookie)必须脱敏,否则违反安全规范
打印时过滤和格式化常见头字段
全量打印所有头既冗余又危险,应按需选取,并统一小写键名便于阅读:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
for _, key := range []string{"User-Agent", "Referer", "X-Forwarded-For", "Content-Type"} {
if vals := headers[key]; len(vals) > 0 {
log.Printf("req_header_%s=%q", strings.ToLower(key), vals[0])
}
}
-
Authorization头必须替换为"<redacted>"</redacted>,不能只截断 -
Cookie头建议整体跳过,或仅保留sessionid=类标识字段(需正则提取) - 注意
X-Real-IP和X-Forwarded-For可能为空,需判空再取首项
注意中间件执行顺序对头可用性的影响
如果你用了 gorilla/handlers 或自定义压缩/重写中间件,它们可能在你日志中间件之前就消费或修改了 r.Header。例如:
-
handlers.CompressHandler会删掉Accept-Encoding并加Vary - 某些反向代理中间件会覆盖
RemoteAddr,但不会动Header,所以头本身不受影响 - 务必把日志中间件放在链最外层(即第一个中间件),否则可能错过原始头
最稳妥的做法:日志中间件作为最外层包装,且只依赖 r 初始化时的状态,不依赖任何后续中间件的副作用。


















