Go的zip.Writer默认使用zip.Store(无压缩),需为每个文件头显式设置header.Method = zip.Deflate才能启用Deflate压缩,否则文件体积不变。

zip.Writer默认不压缩,header.Method = zip.Deflate必须显式设置
你生成的 ZIP 文件体积没变小?不是代码写错了,是 Go 默认用 zip.Store —— 纯归档、零压缩。它只是把文件原样打包进去,连 ZIP 后缀都是“骗人”的。
-
zip.Writer对每个文件都默认走Store,哪怕你调了CreateHeader也不自动切到压缩模式 - 必须为每个
zip.FileHeader单独设header.Method = zip.Deflate - 空文件或极小文件(Deflate,底层 zlib 也可能回退到
Store,这不是 bug,是合理优化 - 已压缩格式(
.jpg、.png、.mp4)再压基本无效,甚至略增体积;文本类(.go、.json)才真正受益
解压时路径没校验,../../../etc/passwd 就能写进系统目录
archive/zip 完全不拦路径穿越 —— 它把 f.Name 当普通字符串拼接,你传 "../../config.yaml",它就真往你项目上层写。
- 必须对每个
f.Name先调filepath.Clean(f.Name)归一化(./a/../b→b,../x→../x) - 检查归一化后是否仍含
".."或以"/"开头(Windows 下还要统一转小写、把"\"换成"/") - 最终目标路径得用
filepath.Join(destDir, cleanName)构造,再用strings.HasPrefix(filepath.ToSlash(absDst), filepath.ToSlash(absDest))二次确认没逃出根目录 - 跳过
f.IsDir()为 true 的条目?不行 —— 目录也要创建,否则解压后结构错乱;但得确保cleanName以"/"结尾,再os.MkdirAll(..., 0755)
io.Copy 直接怼整个 zip.File,OOM 和静默损坏一起发生
别信 “io.Copy(dst, f.Open()) 一行解压” 这种写法。它既危险又低效:
- 不校验 CRC32,损坏 ZIP 也能“成功”解出垃圾数据
- 大文件(几十 MB)会一次性读进内存,runtime 直接 OOM
- 正确做法:用
f.Open()得到io.ReadCloser,再io.CopyBuffer(dst, src, make([]byte, 32*1024))控制缓冲区大小 -
zip.File.Open()返回的io.ReadCloser必须显式Close(),否则循环解多个 ZIP 时很快触发"too many open files"
中文文件名在 Windows 上乱码,header.Flags = 1 是关键开关
Go 默认按 UTF-8 写文件名,但 Windows 资源管理器默认用 GBK 解码 —— 两边不匹配,名字就变 .txt。
立即学习“go语言免费学习笔记(深入)”;
- 写 ZIP 时,给每个
header加一句header.Flags = 1,表示文件名是 UTF-8 编码 - Go 1.22+ 可用
header.SetUTF8(true)替代,更语义化 - 不要试图用
golang.org/x/text/encoding转 GBK:Windows 能认,Linux/macOS 解压就乱码 —— 没银弹,按目标用户环境选 - 别漏掉
header.SetModTime(time.Now()),否则所有文件时间戳一样,看着就很假
最常被忽略的其实是三件事:解压前路径校验、解压后 io.ReadCloser.Close()、压缩时 header.Method 手动设 Deflate。其他问题都能查文档,这三条踩中任意一个,轻则功能异常,重则线上被利用。


















