Gin默认不压缩响应,因其设计上将压缩视为传输层优化,应由反向代理(如Nginx)或显式中间件控制;应用层启用易致CPU开销增加、HTTP/2流控干扰及重复压缩失败。

为什么Gin默认不压缩响应?
Gin本身不内置HTTP响应压缩逻辑,net/http标准库虽支持gzip,但需显式启用且要求客户端声明Accept-Encoding: gzip,而Gin的c.JSON()、c.String()等方法直接写入http.ResponseWriter,绕过了自动压缩路径。你看到响应没压缩,不是配置漏了,而是根本没触发压缩机制。
要让压缩生效,必须在响应写入前完成两件事:拦截响应体、用gzip.Writer或zlib.Writer包装输出流。Gin没有中间件能直接“重写”ResponseWriter,所以得自己封装一个带压缩能力的ResponseWriter实现。
如何用中间件实现Gzip压缩?
最稳妥的做法是写一个中间件,替换c.Writer为支持gzip的自定义ResponseWriter。核心是判断c.Request.Header.Get("Accept-Encoding")是否含gzip,再决定是否包装。
常见错误是直接对整个ResponseWriter做gzip.NewWriter()包装后返回——这会导致Header提前写入失败(WriteHeader()被跳过),最终返回500或空响应。正确做法是继承gin.ResponseWriter接口,重写Write()和WriteHeader(),并在首次Write()时初始化gzip.Writer。
- 只对
Content-Type匹配text/、application/json、application/javascript等可压缩类型启用压缩,避免压缩图片、PDF等二进制内容 - 设置
Content-Encoding: gzipHeader必须在WriteHeader()之后、Write()之前手动添加,否则浏览器无法解压 - 压缩级别建议用
gzip.BestSpeed(1)或gzip.DefaultCompression(6),别用BestCompression(9),CPU开销大且收益有限 - 务必调用
gz.Close(),否则部分数据可能滞留在缓冲区未写出
func GzipMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
if !strings.Contains(c.Request.Header.Get("Accept-Encoding"), "gzip") {
c.Next()
return
}
c.Header("Vary", "Accept-Encoding")
gz, err := gzip.NewWriterLevel(c.Writer, gzip.BestSpeed)
if err != nil {
c.Next()
return
}
gzWriter := &gzipResponseWriter{
ResponseWriter: c.Writer,
gz: gz,
wroteHeader: false,
}
c.Writer = gzWriter
c.Next()
gz.Close()
}
}
type gzipResponseWriter struct {
gin.ResponseWriter
gz *gzip.Writer
wroteHeader bool
}
func (w *gzipResponseWriter) Write(data []byte) (int, error) {
if !w.wroteHeader {
w.WriteHeader(200)
}
return w.gz.Write(data)
}
func (w *gzipResponseWriter) WriteHeader(code int) {
if w.wroteHeader {
return
}
w.wroteHeader = true
w.ResponseWriter.Header().Set("Content-Encoding", "gzip")
w.ResponseWriter.WriteHeader(code)
}
为什么用了中间件还是没压缩?
最常踩的坑是:中间件注册顺序不对,或响应体太小触发了gzip的默认阈值(gzip包内部对小于55字节的数据不压缩)。另外,如果上游代理(如Nginx)已做了压缩,后端再压一次会导致双重压缩,浏览器解压失败。
验证是否生效,别只看Content-Encoding Header——用curl -H "Accept-Encoding: gzip" -I http://localhost:8080/api检查Header,再用curl -H "Accept-Encoding: gzip" http://localhost:8080/api | wc -c对比未压缩请求的字节数。若差值很小,大概率是响应体未达压缩阈值,或Content-Type被误判为不可压缩类型(比如返回application/octet-stream)。
- 确保
c.Writer.Header().Get("Content-Type")在WriteHeader()前非空;Gin默认对c.JSON()设application/json; charset=utf-8,没问题;但c.Data()不会自动设Type,得手动c.Header("Content-Type", "application/json") - 不要在中间件里调用
c.Abort()后还试图写响应,gzip.Writer可能已关闭 - 若用
gin-contrib/cors等第三方中间件,把它放在压缩中间件之后,否则CORS Header可能被压缩逻辑干扰
要不要用第三方库如gin-gzip?
gin-gzip确实省事,但它把压缩逻辑硬编码进中间件,不支持细粒度控制(比如按路由开关、按size阈值跳过)。更麻烦的是,它内部用bytes.Buffer暂存响应体再整体压缩,内存占用随响应体线性增长——一个10MB的文件下载接口会吃掉至少20MB内存(Buffer + gzip输出),而流式压缩(如上面手写的)内存占用恒定在几KB。
真正需要压缩的场景,通常是API返回的JSON或HTML文本,体积在1KB–500KB之间。这种情况下,手写中间件更轻量、更可控。如果项目里已有大量gin-gzip调用且稳定运行,没必要换;但新项目或对性能敏感的服务,优先选流式、可定制的实现。
立即学习“go语言免费学习笔记(深入)”;
实际部署时,注意反向代理(如Nginx)通常也开启gzip,此时应关掉Go层压缩,避免重复压缩浪费CPU——关键是看谁离客户端更近。如果Gin直面公网,Go层压缩更可控;如果前面有CDN或Nginx,交给它们处理更高效。


















