必须显式设置zip.FileHeader.Name为Unix风格相对路径(/分隔、不以/开头),用filepath.Rel和filepath.ToSlash归一化;目录项需以/结尾且Mode().IsDir()为true;须手动设header.Method=zip.Deflate启用压缩,否则仅归档无压缩效果。

压缩时如何正确设置 ZIP 内部目录结构
archive/zip 不会自动推导或补全目录层级,zip.FileHeader.Name 必须显式设为 Unix 风格相对路径(/ 分隔、不以 / 开头),否则 zip.Writer.CreateHeader 会静默失败或生成损坏 ZIP。
- 用
filepath.Rel(root, fullPath)获取相对于压缩根的路径,再调用filepath.ToSlash(filepath.Clean(relPath))归一化 - 目录项必须以
/结尾,且header.Mode().IsDir()为 true;否则解压端可能忽略该条目或无法建空目录 - 不要直接用
filepath.Base()—— 它丢弃所有父级,导致 ZIP 内无任何目录结构 -
header.Method = zip.Deflate必须手动设置,否则只是无压缩归档(体积不变、无压缩效果)
解压时如何安全还原目录结构并防 Zip Slip
ZIP 文件中任意 FileHeader.Name 都可含 ../,不做校验就拼接写入等于开放任意路径写入。Windows 资源管理器和 unzip -t 均不拦截这类恶意路径。
- 对每个
f.Name先调用filepath.Clean(f.Name),再检查是否仍含".."或以"."开头(如"./etc/passwd") - 更稳妥做法:计算目标绝对路径
absDst := filepath.Join(dstDir, cleanedName),再用strings.HasPrefix(filepath.ToSlash(absDst), filepath.ToSlash(expectedBase))校验是否仍在允许范围内 - 遇到
f.IsDir()为 true 的条目,必须调用os.MkdirAll(filePath, 0755);ZIP 不保证父目录先出现,不能依赖顺序 - 中文文件名需确保
header.Flags |= 0x800(写入时)且 Go 1.22+ 解压端调用r.RegisterDecompressor(zip.Deflate, ...)配合zip.NewReader的 UTF-8 支持
tar.gz 场景下目录结构由 tar.Header 控制,gzip 只负责压缩流
gzip.Writer 和 tar.Writer 是嵌套关系,不是先后执行。目录结构完全由 tar.Header 的 Name 和 Typeflag 决定,gzip 层只管字节流压缩。
-
tar.Header.Name必须是相对路径(如"config/app.yaml"),Windows 路径先过filepath.ToSlash() - 目录项设
Typeflag = tar.TypeDir,且Name末尾带/;普通文件用tar.TypeReg -
tar.Writer.Close()必须在gzip.Writer.Close()之前,否则 gzip 尾部 CRC/ISIZE 缺失,gunzip -t报unexpected end of file - 遍历源目录请用
filepath.WalkDir(非filepath.Walk),避免符号链接循环 panic
为什么 zip.Writer.CreateHeader 返回 error 却容易被忽略
这个函数在 Name 含非法字符(如 ../、C:\、:)、为空、或以 / 开头时直接返回 error,但不会 panic,也不会中断后续循环——结果是 ZIP 看似生成成功,实际内部结构已损坏。
立即学习“go语言免费学习笔记(深入)”;
- 每次调用后必须写
if err != nil { return err },不能只 log 后继续 - 错误信息通常是
invalid header field或invalid name,但不会告诉你哪一项出错,建议提前对relPath做白名单过滤(如^[a-zA-Z0-9._/-]+$) - 测试时用
unzip -l xxx.zip和unzip -t xxx.zip验证结构完整性,别只看文件是否生成
tar.Writer.Close() 和 gzip.Writer.Close() 的先后,以及 zip.Writer.CreateHeader 的 error 检查——漏掉任一环节,生成的压缩包大概率在下游工具里打不开。


















