go标准库默认不启用http响应压缩,因其设计追求极简与显式控制,避免隐式行为干扰调试、双压缩及流式响应误处理;必须手动集成压缩中间件并在writeheader阶段检查accept-encoding并同步设置content-encoding和vary头。

Go 标准库本身不自动启用 HTTP 响应压缩,必须手动集成压缩中间件或包装 http.ResponseWriter;直接调用 gzip.Writer 而不处理 Content-Encoding、Vary 头或状态码拦截,会导致浏览器解压失败或缓存错乱。
为什么 net/http 默认不压缩响应
Go 的 http.Server 设计上保持极简,默认不干预响应体内容。它不会检查 Accept-Encoding 请求头,也不会自动包装 ResponseWriter 为 gzip.Writer 或 zstd.Writer。这不是遗漏,而是权衡:避免隐式行为干扰调试、防止双压缩(如反向代理已压缩时再压)、规避对流式响应(如 text/event-stream)的误处理。
常见错误现象:curl -H "Accept-Encoding: gzip" http://localhost:8080/ 返回未压缩内容,但响应头含 Content-Encoding: gzip —— 这是典型的手动设置头但没真正压缩数据,浏览器会解压失败并报 ERR_CONTENT_DECODING_FAILED。
- 压缩必须在写入响应体前完成,且需同步设置
Content-Encoding和Vary: Accept-Encoding - 不能在
WriteHeader()后才决定是否压缩,否则状态码已发送,无法再改头 - 若 handler 中提前调用
w.WriteHeader(200),后续w.Write()将跳过头写入逻辑,导致压缩头丢失
用 golang.org/x/net/http2/h2c + 自定义 ResponseWriter 实现安全压缩
推荐封装一个带条件判断的 compressResponseWriter,它在 WriteHeader() 阶段就检查 Accept-Encoding 并决定是否启用 gzip/zstd,避免后期写入时状态已定。
关键点:
- 继承
http.ResponseWriter接口,重写WriteHeader()、Write()、Header() - 在
WriteHeader()中解析请求头,匹配gzip、br、zstd(注意:标准库不支持br和zstd,需引入第三方如cloudflare/compress) - 首次
Write()时初始化压缩 writer,并替换底层ResponseWriter的Write方法 - 务必调用
Close()(如gzip.Writer.Close())确保尾部写入,否则 gzip 流不完整
示例片段(仅 gzip):
type gzipResponseWriter struct {
http.ResponseWriter
writer *gzip.Writer
written bool
}
<p>func (w *gzipResponseWriter) WriteHeader(statusCode int) {
if statusCode </p><p>func (w *gzipResponseWriter) Write(p []byte) (int, error) {
if !w.written {
w.WriteHeader(http.StatusOK)
}
if w.writer != nil {
return w.writer.Write(p)
}
return w.ResponseWriter.Write(p)
}
</p>
使用 rs/cors 或 go-chi/chi 等中间件时的压缩陷阱
很多中间件(如 cors.Handler、chi.Middlewares)会包装原始 ResponseWriter,若压缩中间件放在它们之后,可能收不到原始 Header() 修改权限;若放之前,则 CORS 头可能被压缩器忽略(因压缩器只管 Content-* 相关头)。
正确顺序必须是:压缩中间件 → CORS/日志/认证等中间件 → handler,否则 Vary: Accept-Encoding 可能被覆盖或丢失。
-
chi用户应使用chi.Wrap而非chi.Use,确保压缩器包裹整个链路 - 若 handler 内部调用
http.Redirect(),它会直接写状态码和Location头,绕过你的压缩 writer —— 此时需在压缩器中拦截3xx状态并禁用压缩 - 对
application/json压缩收益高,但对已压缩资源(如.jpg、.woff2)再压反而增大体积,应在shouldCompress()中排除image/.*、font/.*
生产环境必须检查的三个压缩细节
上线前漏掉任一细节都可能导致部分用户白屏或 API 解析失败:
-
Accept-Encoding解析要区分大小写?RFC 要求不区分,但某些旧客户端发accept-encoding小写,建议用strings.Contains(strings.ToLower(r.Header.Get("Accept-Encoding")), "gzip") - HTTP/2 下服务器端压缩仍有效,但客户端可能优先用 HPACK 压缩头,不影响响应体压缩逻辑
- CDN(如 Cloudflare)默认开启 Brotli,若你后端也压 gzip,而 CDN 未识别并透传
Vary,会导致缓存污染 —— 必须确保Vary: Accept-Encoding出现在最终响应中
最易被忽略的是:压缩器必须在 WriteHeader() 时就决策,而不是等第一次 Write() 才看请求头。一旦状态码发出,HTTP 协议不允许再修改响应头,此时加 Content-Encoding 已无效,浏览器只会当普通响应体处理,gzip 字节流裸露,必然解压失败。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











