gzip.Handler必须置于最外层,否则因底层ResponseWriter被封装而无法劫持Write/WriteHeader导致压缩失效;仅当状态码为2xx/3xx、Content-Length>1024或未知、且Content-Type可压缩时才生效。

gzip.Handler 必须包在最外层,否则压缩失效
Go 标准库的 gzip.Handler 不是装饰器式中间件,它需要直接接管原始 http.ResponseWriter 的全部写入行为。一旦被日志、CORS、Auth 等中间件包裹在内层,底层 writer 就已被封装(比如加了 header 记录或状态码拦截),gzip.Handler 就无法劫持 Write() 和 WriteHeader() 调用——结果就是响应头里写了 Content-Encoding: gzip,但 body 没压缩,浏览器解压失败,报 ERR_CONTENT_DECODING_FAILED。
正确写法只有一种:
http.ListenAndServe(":8080", gzip.Handler(myMux))
如果你用的是 chi.Router 或自定义 http.Handler 链,确保 gzip.Handler 是最终包装者;静态文件服务(如 http.FileServer)也必须包进去,否则 .js、.css 不会压缩。
哪些响应会被跳过?三个硬条件同时满足才压缩
gzip.Handler 不是“有 gzip 请求头就压”,它内置三道过滤:
立即学习“go语言免费学习笔记(深入)”;
- 响应状态码必须是 2xx 或 3xx(4xx/5xx 错误页默认不压,避免暴露调试信息)
-
Content-Length未知,或已知且 > 1024 字节(小响应压缩收益低,还白耗 CPU) -
Content-Type属于可压缩类型,例如text/html、application/json、text/css;而image/png、application/octet-stream这类本身已压缩的类型会被跳过
常见踩坑:返回 JSON 却设了 text/plain,或 handler 中提前调了 w.WriteHeader(200) 导致 header 锁定,后续没法补 Content-Encoding —— 此时 curl -I -H "Accept-Encoding: gzip" 看不到 Content-Encoding 头,就是没进压缩逻辑。
想跳过健康检查路径或调压缩级别?得自己写 wrapper
gzip.Handler 不接受任何参数,没法加白名单、设级别、排除 /healthz。这时候只能抄它的逻辑,手动用 gzip.NewWriterLevel 包装 ResponseWriter。
关键点在于:
- 必须实现完整
http.ResponseWriter接口,尤其重写WriteHeader()—— 在这里检查r.Header.Get("Accept-Encoding")是否含gzip,并决定是否启用压缩 -
Write()第一次调用时才初始化*gzip.Writer,避免对 304/204 等无 body 响应做无效包装 - 务必在 handler 返回前调
gz.Close(),否则 gzip 流缺少 CRC32 和 ISIZE 尾部,解压端报gzip: invalid checksum - 若需支持
br或zstd,标准库不支持,得引入cloudflare/compress等第三方
流式响应(SSE、chunked)不能直接套 gzip
HTTP 流式接口(如 text/event-stream、长轮询)和 gzip.Writer 天然冲突:gzip 需要缓冲或显式 Flush() 才能输出有效块,而流式要求低延迟分块写出。强行压缩会导致延迟飙升、连接中断,甚至客户端收不到首帧。
唯一稳妥做法是按路径禁用压缩:
- 在自定义 wrapper 的
WriteHeader()中检查w.Header().Get("Content-Type") == "text/event-stream" - 或统一约定:所有 SSE 接口路径带
/stream/前缀,在 wrapper 里用strings.HasPrefix(r.URL.Path, "/stream/")跳过压缩逻辑 - 别试图在
Write()里每块都gz.Flush()—— 这样既没真正压缩(块太小),又破坏流式语义
压缩不是万能开关,它和响应类型、客户端能力、代理行为深度耦合。最常被忽略的其实是反向代理:Nginx 默认会删掉 Vary: Accept-Encoding 头,却不解压 body,导致缓存错乱。上线前务必用真实链路验证整个压缩闭环。


















