← 中文资源

简体中文版本

Gzip 响应压缩中间件

当客户端声明支持 gzip 时压缩 HTTP 响应体,正确设置 Vary 头,并对不支持压缩的客户端保持原样。

阅读英文原文

功能说明

文本类响应压缩后通常只剩原体积的几分之一,能明显降低带宽和传输时间。HTTP 内容协商要求服务端读取 Accept-Encoding,在支持 gzip 时才压缩,并在响应中声明 Content-Encoding。无论是否压缩,都应添加 Vary: Accept-Encoding,告诉缓存同一 URL 可能因请求头不同而返回不同表示,避免把压缩内容发给不支持的客户端。

中间件把原始响应写入器包装成会经过 gzip.Writer 的版本,业务处理器无需感知压缩逻辑。演示用重复文本放大效果,启动服务后可用浏览器或 curl 验证响应头。

关键代码

gzipWriter 内嵌 http.ResponseWriter,只覆盖 Write,让处理器写入的数据进入压缩器;其他方法仍由原始响应器处理。GzipMiddleware 先追加 Vary,再检查 Accept-Encoding 是否包含 gzip,不包含就原样放行。支持时设置 Content-Encoding 并创建默认压缩级别的写入器,用 defer 确保请求结束时刷新尾部数据。业务处理器设置的内容类型和状态码仍通过内嵌接口传递。

使用注意

字符串包含判断会接受 q=0 的 gzip,严格实现应解析权重。压缩前应判断内容类型,图片、视频和已压缩格式再压缩往往更大。处理器若提前设置 Content-Length,压缩后长度会不匹配,应删除该头。包装器没有实现 http.Flusher 和 http.Hijacker,流式响应或 WebSocket 升级会失效,需要按需转发接口。错误响应同样会被压缩,调试时应关闭中间件对照。压缩会消耗 CPU,高并发下应评估成本。

验证方式

启动服务后执行 curl -H “Accept-Encoding: gzip” –compressed 请求根路径,响应头包含 Content-Encoding: gzip 与 Vary,正文为重复的 hello gzip。去掉 Accept-Encoding 再请求,响应不压缩且正文不变。可对比两次响应的字节数,压缩版本应明显更小。

代码示例

package main

import (
	"compress/gzip"
	"fmt"
	"net/http"
	"strings"
)

type gzipWriter struct {
	http.ResponseWriter
	writer *gzip.Writer
}

func (g *gzipWriter) Write(data []byte) (int, error) {
	return g.writer.Write(data)
}

func GzipMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Add("Vary", "Accept-Encoding")
		if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
			next.ServeHTTP(w, r)
			return
		}
		w.Header().Set("Content-Encoding", "gzip")
		compressor := gzip.NewWriter(w)
		defer compressor.Close()
		next.ServeHTTP(&gzipWriter{ResponseWriter: w, writer: compressor}, r)
	})
}

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "text/plain; charset=utf-8")
		fmt.Fprintln(w, strings.Repeat("hello gzip ", 64))
	})
	_ = http.ListenAndServe(":8080", GzipMiddleware(mux))
}