因responsewriter首次write/writeheader即锁定并发送响应头,若直接包裹gzip.writer会导致content-encoding头未设就发头,引发冲突;正确做法是用wrapper缓存body和header,handler执行完再统一压缩写出。

为什么 http.ResponseWriter 不能直接写入压缩流?
因为 http.ResponseWriter 是一个接口,底层实际是 http.response(未导出),它在第一次调用 Write 或 WriteHeader 时会锁定状态并发送响应头。如果中间件直接把 gzip.Writer 包裹它再传给 handler,handler 写入时可能触发 header 发送,而此时压缩头(Content-Encoding: gzip)还没加,导致 header 冲突或被忽略。
正确做法是用一个实现了 http.ResponseWriter 的 wrapper,缓存 body 和 header,等 handler 执行完再统一压缩并写出。
如何手动实现轻量级 GZIP 中间件?
核心是实现三个方法:WriteHeader、Write、Header,并维护一个 bytes.Buffer 缓存响应体。关键点:
-
Header()必须返回原始 response 的 header map,这样 handler 调用w.Header().Set("Content-Type", ...)才能生效 - 首次
Write前若未调用WriteHeader,需补默认状态码200 - 仅当
Accept-Encoding包含gzip且响应体长度 ≥ 1024 字节才启用压缩(避免小响应反而膨胀) - 压缩前必须检查
Content-Type是否可压缩(如跳过image/*、application/octet-stream)
示例骨架:
type gzipResponseWriter struct {
w http.ResponseWriter
buf *bytes.Buffer
gz *gzip.Writer
wrote bool
}
func (w *gzipResponseWriter) WriteHeader(statusCode int) {
w.w.WriteHeader(statusCode)
w.wrote = true
}
func (w *gzipResponseWriter) Write(b []byte) (int, error) {
if !w.wrote {
w.w.WriteHeader(http.StatusOK)
w.wrote = true
}
return w.buf.Write(b)
}
func (w *gzipResponseWriter) Header() http.Header {
return w.w.Header()
}
如何安全地判断是否启用 GZIP?
不能只看 Accept-Encoding 请求头是否含 gzip,还要考虑:
- 客户端是否明确禁用:
Accept-Encoding: identity或包含q=0的gzip;q=0 - 响应状态码是否适合压缩:比如
304 Not Modified、204 No Content不应压缩 - 是否已由上游设置过
Content-Encoding(如反向代理透传),此时不应二次压缩 - 响应体是否为空(
len(buf.Bytes()) == 0)
推荐用 strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") 初筛,再用正则提取 q 值做精细判断(如 regexp.MustCompile(`gzip(?:;q=(\d*\.\d+))?\b`))。
为什么生产环境建议用 go-chi/chi 或 gorilla/handlers?
手写中间件容易漏掉边界情况:
- panic 恢复后无法再写响应(需在 defer 中判断
recover()后是否已写 header) - 大响应体(>10MB)导致内存暴涨,没做 streaming 压缩
- 并发写入
bytes.Buffer无锁保护(虽然 handler 是单 goroutine,但中间件可能被复用) - 没处理
Content-Length头:压缩后长度变化,原值必须删除,否则浏览器可能截断
例如 gorilla/handlers.CompressHandlerLevel 内部用 sync.Pool 复用 gzip.Writer,自动删 Content-Length,还支持自定义压缩级别和 MIME 类型白名单 —— 这些细节自己实现成本高、易出错。
真正需要定制时,优先 fork 成熟库而非从零写;GZIP 压缩逻辑本身不复杂,但与 HTTP 生命周期耦合深,错一点就静默失效。











