tar.gz 压缩解压必须严格遵循 gzip.NewReader→tar.NewReader(读)和 tar.NewWriter→gzip.NewWriter→os.Create(写)的嵌套顺序,且 Typeflag 必须显式设置、Size 必须用 io.CopyN 严格读取、路径需 clean 并校验防止遍历。

tar.gz 压缩必须先 tar 再 gzip,顺序反了就解不开
Go 里没有“一键 tar.gz”函数,archive/tar 和 compress/gzip 是两个独立职责的包:tar 负责打包(合并多个文件+路径结构),gzip 负责压缩(单流字节压缩)。你不能用 gzip.NewWriter 包住一个 tar.Writer 的输出流以外的东西,也不能把未压缩的 *os.File 直接丢给 tar.NewReader —— 那读出来全是乱码。
- 错误写法:
tar.NewReader(f)(f是 .tar.gz 文件)→ 报错archive/tar: invalid tar header或解出空名、零大小文件 - 正确嵌套:
gzip.NewReader(f)→ 得到io.Reader→ 传给tar.NewReader() - 写入时也得严格嵌套:
os.Create→gzip.NewWriter→tar.NewWriter,且tar.Writer必须在gzip.Writer关闭前Close() - 漏掉
tw.Close()或gw.Close(),生成的 .tar.gz 用gunzip -t检查会报unexpected end of file
写 tar header 不设 Typeflag 就算内容对也解不开
tar.Header 的 Typeflag 字段不是可选的,Go 的 tar.Reader 对它做严格校验。默认值是 0(即 \x00),而 GNU tar 会容忍并尝试推测,但 Go 不会——它直接跳过该条目或报错 archive/tar: invalid header。
- 普通文件必须显式写:
hdr.Typeflag = tar.TypeReg - 目录必须写:
hdr.Typeflag = tar.TypeDir,且Name末尾带/(如"logs/"),否则会被当成普通文件处理 - 别依赖
file.Stat().Mode()自动推断类型;Mode控制权限,Typeflag控制条目语义 - 示例:
hdr := &tar.Header{Name: "config.json", Size: 128, Mode: 0644, Typeflag: tar.TypeReg}
解压 tar.gz 时不按 Header.Size 读就会串位
tar.Reader.Next() 返回一个 header 后,后续数据就是这个文件的内容,长度正好是 header.Size。如果你用 io.Copy(dest, tr) 一直读到 io.EOF,就会把下一个文件的 header 也吞进去,导致后续 Next() 解析失败、文件名错乱、甚至 panic。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 安全做法:用
io.CopyN(dest, tr, header.Size),它保证只读指定字节数 - 千万别用
io.ReadAll(tr)或循环tr.Read(buf)直到err == io.EOF—— 这破坏 tar 流结构 - 目录条目(
TypeDir)的Size是 0,此时跳过CopyN,只调os.MkdirAll(...) - 如果要限速或打日志,套
io.LimitReader(tr, header.Size)再传给io.Copy,而不是自己管理缓冲
路径遍历漏洞不校验,../../etc/passwd 就真写进系统了
tar 和 zip 一样,Header.Name 完全由打包方控制。Go 标准库不做任何路径 sanitize,filepath.Join(dstRoot, header.Name) 一旦遇到 "../",就可能覆盖任意位置。这不是理论风险,而是真实 CVE 场景。
立即学习“go语言免费学习笔记(深入)”;
- 必须先
cleanPath := filepath.Clean(header.Name) - 再检查:
cleanPath != header.Name || strings.HasPrefix(cleanPath, "..") || strings.HasPrefix(cleanPath, "/")→ 直接跳过该条目 - 创建文件前,用
os.MkdirAll(filepath.Dir(dstPath), 0755)确保父目录存在;目录条目要单独判断header.Typeflag == tar.TypeDir并os.MkdirAll(dstPath, header.Mode) - Windows 路径分隔符也要处理:
filepath.ToSlash(header.Name)统一后校验,避免"..\foo"绕过
最常被忽略的一点是:所有 Close() 都不能靠 defer 在同一函数里混用读写句柄——比如写完 .tar.gz 立刻用 os.Open 读它,此时 defer gw.Close() 还没执行,文件内容可能还在内核缓冲里。分阶段、显式 Close(),才是流式归档稳住的关键。

















