gzip.writer.close() 必须调用,否则压缩流不完整——因crc校验值和原始长度isize仅在close()时写入,导致解压端报unexpected end of file、eoferror或invalid checksum。

直接对大 JSON 字符串做 gzip.Writer 压缩后写入 HTTP 响应体,不调用 Close() 就返回,解压端 100% 失败——不是客户端问题,是压缩流未完成。
为什么 gzip.Writer.Close() 不调用就一定出错
现象包括:gunzip -t 报 unexpected end of file、Python 的 gzip.open() 抛 EOFError、Go 解压时提示 gzip: invalid checksum。根本原因不是数据没写完,而是 gzip.Writer 内部缓冲未刷新,且 GZIP 尾部的 CRC 校验值和原始未压缩长度(ISIZE)只在 Close() 时写入。哪怕你用 io.Copy 把全部字节都塞进去了,只要没 Close(),这个流就不合法。
常见错误写法:
-
io.Copy(w, bytes.NewReader(data)); return—— 没Close(),数据一定不完整 -
defer w.Close()放在函数开头,但中间有return或 panic ——Close()被跳过
正确做法:所有写操作完成后,立刻调用 w.Close() 并检查返回 error(比如磁盘满、网络断开导致写失败)。
大 JSON 字符串不能全量加载进 bytes.Buffer
几百 MB 的 JSON 直接 json.Marshal → bytes.Buffer → gzip.Writer,极易 OOM。这不是算法问题,是 bytes.Buffer 底层切片无节制扩容导致的内存爆炸。
可行路径只有「流式分块」:
- 把大 JSON 按语义或固定大小(如 1MB)切片,每块单独压缩、发送
- 若必须暂存压缩块(例如分片上传),每次
w.Write()后手动buf.Reset(),并在该块压缩完毕后立即w.Close() - 别复用
bufio.Reader返回的底层[]byte切片——它会被后续读取覆盖;每次写入前必须copy()出独立副本
HTTP 流式响应中 gzip.Writer 必须禁用缓冲并显式 Flush
默认 gzip.NewWriter 使用 32KB 缓冲区,且只在 Close() 时输出 header 和 footer。这对 HTTP 流是灾难:客户端永远收不到第一个字节,直到整个响应结束。
解决方法是绕过默认行为:
- 用
gzip.NewWriterLevel(w, gzip.NoCompression)创建 writer(level 0 减少 CPU 开销,也加快 write 返回) - 每次
w.Write(chunk)后紧跟w.Flush(),确保数据立即推到http.ResponseWriter -
defer w.Close()只用于释放资源,不是为了写 footer;流式传输中 footer(0x0000ffff)可被忽略,某些 client 却会校验失败
注意:Content-Encoding: gzip 头必须在首次 Write() 前设置,否则 header 已发送无法修改。
解压端必须用 gzip.NewReader 包裹,不能直接读原始 Reader
常见错误是拿到 *os.File 或 http.Response.Body 后,直接用 io.ReadAll(r) 或 bufio.Scanner 读取,结果得到的是乱码二进制流,而非原始 JSON 字符串。
正确做法始终是:
- 先用
gzip.NewReader(r)包一层,再交给下游解析器 - 如果输入是 HTTP 流(可能头未收全),需先用
bufio.NewReaderSize(r, 4096)缓冲,Peek(10)确认前两字节为0x1f 0x8b,再传给gzip.NewReader - 不要依赖
Transfer-Encoding: chunked—— 它不保证 gzip 帧完整性;更稳妥的是服务端写 4 字节长度前缀 + gzip 数据(无 footer),客户端按长读取后构造bytes.NewReader给gzip.NewReader
最易被忽略的点:gzip 流的完整性不靠连接是否关闭来判断,而靠 Close() 是否成功执行、以及解压端是否严格按 GZIP 格式消费。任何环节跳过 Close() 或误用 reader,都会让整个链路失效。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










