go的archive/zip包默认仅归档不压缩,必须为每个文件显式设置header.method = zip.deflate启用deflate压缩;空/小文件或已压缩格式再压无效;解压需filepath.clean校验路径防zip slip,中文名需flags=1或setutf8(true)。

Go 的 archive/zip 包本身不压缩,只归档;你生成的 ZIP 文件体积没变小,不是代码写错了,是默认用了 zip.Store —— 它连 zlib 都不调用,就是原样打包。
为什么 zip.Writer 默认不压缩?
因为 Go 标准库把“归档”和“压缩”拆开了:zip.Writer 负责组织文件结构,而压缩是可选动作。默认所有文件都走 zip.Store(无压缩存储),必须为每个文件显式设置 header.Method = zip.Deflate 才启用 DEFLATE。
-
zip.Deflate是唯一受支持的压缩方法;zip.BestCompression这类常量根本不存在 - 空文件或极小文件(如 Deflate,底层 zlib 也可能自动回退到
Store,这不是 bug - 已压缩格式(JPEG/PNG/MP4)再压基本无效,还可能略增体积;文本类(JSON、Go 源码)才明显受益
- 漏掉
zw.Close()会导致 ZIP 文件非法 —— 中央目录区没写入,很多解压工具会静默失败
解压时怎么防 ../../../etc/passwd 覆盖系统文件?
Go 不做任何路径拦截,f.Name 原样拼进 filepath.Join(dst, f.Name) 就是 Zip Slip 漏洞。必须对每个条目做两层校验:
- 先调用
filepath.Clean(f.Name)归一化(./a/../b→b,../x→../x) - 再检查:
cleanPath != f.Name || strings.HasPrefix(cleanPath, "..") || strings.HasPrefix(cleanPath, "/") - 最后确认目标路径确实在输出目录下:
strings.HasPrefix(filepath.ToSlash(dstPath), filepath.ToSlash(absDest)) - Windows 下还要统一转小写、替换
"\"为"/",否则"..\..\etc\passwd"可能绕过
中文文件名乱码怎么办?
不是 Go 解析错了,是 ZIP 包本身用了 GBK 编码(常见于 Windows 资源管理器或旧版 7-Zip)。archive/zip 始终按 UTF-8 解析 FileHeader.Name,结果就是一堆 .txt。
- 最优解:让上游改用 UTF-8 打包(7-Zip 勾选 “UTF-8”,macOS 默认,
zip -U) - 若必须兼容,用
golang.org/x/text/encoding/simplifiedchinese手动 decode:decoded, _ := gbk.NewDecoder().String(f.Name) - Go 1.22+ 可尝试
zr.RegisterDecompressor(zip.FormatUTF8, ...),但非所有 ZIP 工具都设对 flags 位,不可依赖
大文件解压卡死或 OOM 怎么办?
常见错误是先把整个 ZIP 读进内存:data, _ := io.ReadAll(zipFile); zip.NewReader(bytes.NewReader(data), ...)。一个 500MB ZIP 可能吃掉 1.2GB 内存。
- 压缩时:用
os.Create创建目标文件,传给zip.NewWriter,逐个CreateHeader+io.Copy,不缓存原始文件内容 - 解压时:对每个
f调用f.Open()得到io.ReadCloser,直接io.Copy(dstFile, rc)流式写入磁盘 - 绝对不要对每个条目调用
io.ReadAll(rc)—— 这等于把全部内容加载进内存 -
zip.OpenReader后必须defer r.Close(),否则文件描述符泄露,几百个文件就触发 “too many open files”
真正容易被忽略的是:路径净化必须在 filepath.Join 之前做,且要同时处理大小写、斜杠方向、绝对路径前缀;中文名兼容不是加个 flag 就行,得看 ZIP 包怎么生成的;而大文件流式处理的边界——比如 rc.Close() 必须在每次 io.Copy 后立即执行,不是整个循环结束才关。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











