一个靠谱的 gzip 中间件需在 writeheader 后或首次 write 前判断 accept-encoding 并设置 content-encoding 和 vary 头,用 bytes.buffer 缓存并设阈值(如 ≥1024 字节),跳过 204/304、二进制类型、set-cookie 响应,调用 gzwriter.close(),避免手动设 content-length;brotli 可选但需注意 cdn 是否已启用。

怎么写一个靠谱的 gzip 中间件
Go 标准库不带开箱即用的 gzip 中间件,但也不需要第三方依赖——自己写一个 30 行以内的 wrapper 就够用,关键是把 WriteHeader 和 Write 的时机、顺序、头设置都卡准。
- 必须在第一次
Write前或WriteHeader后立即决定是否启用 gzip,否则响应头已发送,Content-Encoding加不上 - 检测请求头用
r.Header.Get("Accept-Encoding"),别用strings.Contains粗暴匹配,要支持gzip, deflate, br这种逗号分隔格式(建议用strings.FieldsFunc(v, func(r rune) bool { return r == ',' || r == ' ' })拆) - 创建
gzip.NewWriter(w)后,立刻调用gzWriter.SetLevel(gzip.DefaultCompression),别用BestCompression——它会让首字节延迟飙升,API 场景完全没必要 - 务必在 handler 返回前调用
gzWriter.Close(),漏掉这句会导致 gzip 流不完整,客户端解压失败,报错是gzip: invalid header或invalid compressed data--format violated
哪些响应不该压缩
盲目压缩所有响应反而拖慢服务,尤其对小数据或本就压缩过的资源,CPU 白花、体积还可能变大。
- 跳过状态码为
204、304的响应——它们没有响应体,压缩器会 panic 或静默失败 - 跳过
Content-Type是image/*、video/*、application/pdf、application/zip的响应,这些格式本身已是高压缩态 - 设置最小响应体阈值(比如 ≥1024 字节),小响应(如
{"ok":true})压缩后反而更大,且增加 GC 压力;可用bytes.Buffer先缓存,达到阈值再启动gzip.Writer - 跳过带
Set-Cookie头的响应——某些反向代理(如旧版 Nginx)会因 Vary 处理不当导致缓存污染,虽不是 Go 问题,但值得规避
Content-Encoding 和 Vary 头为什么不能少
这两个头不是可选项,而是 HTTP 缓存协商的契约。缺了它们,CDN、浏览器、代理层会把压缩版和未压缩版当成同一份内容缓存,结果用户拿到乱码或解压失败。
-
Content-Encoding: gzip必须在WriteHeader后、首次Write前设置,且只能设一次;重复设置会被忽略 -
Vary: Accept-Encoding必须同步设置,告诉所有中间层:“这个响应体是否压缩,取决于请求里的Accept-Encoding” - 别手动设置
Content-Length——gzip 后长度未知,Go 会自动切到Transfer-Encoding: chunked,强行设会冲突报错
要不要上 Brotli?br > gzip 的真实收益
现代浏览器(Chrome 50+、Firefox 44+、Safari 11+)全量支持 br,压缩率比 gzip 高 15%~20%,解压更快,但 Go 标准库不内置,得引入 github.com/andybalholm/brotli。
立即学习“go语言免费学习笔记(深入)”;
- 中间件里按
Accept-Encoding优先级选算法:先找br,再找gzip,最后 fallback 到明文;别只认gzip -
brotli.NewWriterLevel(w, 4)是推荐起点,比默认11更快,压缩率损失极小 - 注意 brotli writer 同样要
Close(),且它的内存占用略高于 gzip,高并发小响应场景需压测验证 - 如果已有 CDN(如 Cloudflare),确认它是否已自动开启 Brotli——重复压缩不会报错,但浪费 CPU
最常被忽略的点是:Vary 头必须随实际编码动态更新。用 br 就写 Vary: Accept-Encoding,但不能写成 Vary: Accept-Encoding, User-Agent 这种过度泛化形式,否则缓存命中率断崖下跌。











