必须调用 zip.writer.close() 以写入 eocd 记录,否则 zip 解压失败;路径需 clean 并校验前缀防止越界;name 必须为 unix 风格相对路径;按 mime 类型选择 store 或 deflate;禁止并发写 zip.writer。

zip.Writer.Close() 必须调用,否则 ZIP 解压失败
浏览器下载的 ZIP 双击无反应、7-Zip 提示“无法打开存档”、macOS 归档工具静默失败——根本原因不是代码没写完,而是漏掉了 zipWriter.Close()。ZIP 格式末尾必须有 EOCD(End of Central Directory)记录,而只有 Close() 才会刷新缓冲、写入该结构。Flush() 不行,它只刷缓冲,不写 EOCD。
常见错误写法:defer zipWriter.Close() 看似安全,但如果中间发生 panic 且未 recover,函数提前退出,defer 不会执行;更稳妥的是在所有文件写入完成后、return 前显式调用一次:
- 所有
io.Copy()完成后,立即执行zipWriter.Close() - 若使用
defer,确保函数内无未捕获 panic,或搭配recover()使用 - HTTP handler 中不要依赖
defer做唯一保障,因为响应流一旦写出部分字节,客户端就可能开始解压,此时缺 EOCD 就是非法 ZIP
路径必须 clean + 校验,防止越界解压
ZIP 内部的 Name 字段直接控制解压时的文件系统写入路径。传入 /etc/passwd 或 ../config.yaml,解压时可能覆盖服务端关键文件。
正确做法是对每个原始路径做两步处理:
- 先用
filepath.Clean()规范化路径,消除./、../和冗余分隔符 - 再检查是否以允许根目录为前缀,例如
strings.HasPrefix(cleaned, "./files/") -
w.CreateHeader()或w.Create()中传入的Name必须是 Unix 风格相对路径(正斜杠/),Windows 下filepath.Rel()返回反斜杠,需用strings.ReplaceAll(rel, "\", "/")替换 - 拒绝空字符、控制字符、设备名(如
NUL、CON)及危险扩展名(如.sh、.exe)
Store 还是 Deflate?按文件类型选压缩策略
默认 zip.Writer 对所有文件用 zip.Deflate,但小文件或已压缩格式(如 .jpg、.png、.mp4)开启 Deflate 只增 CPU 开销,几乎不减体积。
建议根据 MIME 类型动态设置 Method:
- 对
image/*、video/*、application/gzip等,设header.Method = zip.Store(仅归档,跳过压缩) - 对文本类大文件(日志、CSV、JSON),保留
zip.Deflate - 用
mime.TypeByExtension(ext)判断类型,比单纯看扩展名更可靠 - Go 标准库不支持自定义压缩级别;如需 zlib level 控制,得换
github.com/klauspost/compress
大批量文件不能并发写 zip.Writer
多个 goroutine 同时调用 zw.Create() 或 io.Copy() 到同一个 zip.Writer,必然导致 ZIP 损坏:中央目录错乱、校验和失效、解压报 “central directory not found”。
协程池只用于并行读取文件,不是为了并发写 ZIP:
- 开 N 个 worker goroutine 负责
os.Open+ 读取 + 准备数据,通过 channel 发送给 writer goroutine - writer goroutine 单独持有
zip.Writer,顺序消费 channel 中的文件内容并写入 - worker 读完即
file.Close(),writer 不负责关源文件 - 总并发数建议设为 4–8,避免触发 ulimit -n 或磁盘随机 IO 暴增
最易被忽略的一点:路径净化和 Close() 都做了,但忘了 zip.FileHeader.Name 是相对路径——哪怕只差一个 . 或 /,解压目标就可能偏移到意料之外的位置。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











