必须显式调用gzip.writer.close(),否则生成的.gz文件损坏;因crc和isize仅在close()时写入末尾,漏调用会导致gunzip -t报unexpected end of file,稳妥做法是io.copy后立即检查w.close()错误。

compress/gzip 流式压缩大文件必须显式 Close()
不调用 gzip.Writer.Close(),生成的 .gz 文件一定损坏——gunzip -t 会报 unexpected end of file。这是因为 CRC32 校验值和 ISIZE 字段只在 Close() 时写入末尾。
常见错误是只写 defer gzWriter.Close(),但函数中途 return 或 panic 会导致 defer 不执行。稳妥做法是在 io.Copy() 后立刻检查:
if err := gzWriter.Close(); err != nil { return err }- 压缩级别可选:
gzip.NoCompression(快)、gzip.BestSpeed(日志场景友好)、gzip.DefaultCompression(通用) - 大文件建议配大缓冲区:
buf := make([]byte, 1(1MB),传给 <code>io.CopyBuffer(dst, src, buf)
archive/zip 解压大 ZIP 包容易 OOM
archive/zip 默认把整个 ZIP 加载进内存解析结构,几百 MB 的 ZIP 就可能触发 OOM。这不是 bug,是设计使然——它要一次性读取中央目录(CDIR)才能遍历文件列表。
真正可行的流式解压方式是:用 zip.OpenReader 打开后,对每个 zip.File 调用 f.Open() 得到 io.ReadCloser,再用 io.Copy 直接写入磁盘文件,全程不缓存内容。
- 禁止用
bytes.Buffer中转:io.Copy(&buf, rc)会把整个文件读进内存 - 目录创建必须提前:
os.MkdirAll(filepath.Dir(dstPath), 0755) - 务必先判断
f.IsDir(),否则对目录调f.Open()会返回invalid argument
gzip vs zip:选错格式等于重写一半逻辑
混淆 compress/gzip 和 archive/zip 是最常踩的坑。前者只做单文件流式压缩(如 app.log.gz),后者是打包+压缩一体化(支持多文件、目录结构、Deflate 压缩)。
- 想压缩
config.yaml + images/icon.png→ 必须用archive/zip - 只压缩一个 2GB 日志 →
compress/gzip更轻量、更可控 - 要发
tar.gz?得先用archive/tar打包成流,再套gzip.Writer——顺序反了,解压就失败
中文文件名和路径穿越是 ZIP 最易漏的安全点
archive/zip 默认用 CP437 编码写文件名,中文名在 Windows/macOS 上显示为乱码或 .txt 后缀。根本原因是没设 UTF-8 标志位:fh.Flags |= 0x800。
而解压时若不对 f.Header.Name 做路径清洗,攻击者塞个 ../../../etc/passwd 就能覆盖系统文件。
- 压缩前:用
filepath.Rel(rootDir, fullPath)得相对路径,再strings.ReplaceAll(relPath, "\", "/") - 解压时:先
filepath.Clean(f.Header.Name),再检查是否含".."或以".."开头 - 最终路径必须用
filepath.Join(destDir, cleanName)构造,并用filepath.Rel(destDir, absDst)验证是否仍在目标目录内
路径校验和 Close() 调用这两个点,90% 的线上 ZIP/GZIP 故障都出在这儿——不是算法慢,是逻辑漏了关键防护或收尾动作。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











