Go tar 怎样打包目录并保留模式
来源:17golang原创
时间:2026-10-22 00:34:53 391浏览 收藏
我在做备份导出时,最容易踩的坑不是把文件写进 tar,而是归档里到底保存了什么:绝对路径会污染部署环境,直接拼路径会漏掉目录模式,符号链接又不能当普通文件读取。比较稳的做法是用 filepath.WalkDir 遍历,用 tar.FileInfoHeader 生成头部,再把路径改成相对路径;普通文件复制内容,目录只写头部,符号链接保存目标。
官方地址:https://pkg.go.dev/archive/tar
FileInfoHeader会把文件类型、大小和模式带入 tar 头部,但归档名称要自己改成相对路径。- 目录不需要
io.Copy;符号链接应通过Linkname保存目标,不能跟随链接读取内容。 - tar 头部保留的是模式信息,解包后的最终权限仍可能受到 umask、解包工具和目标系统规则影响。
先把模式和归档路径分开处理
archive/tar 的 Writer 是顺序写入器。每个条目先调用 WriteHeader,之后才写该条目的内容。FileInfoHeader 可以从 fs.FileInfo 生成部分头部,其中包括模式、大小和文件类型;但 FileInfo.Name() 只有基本名,所以遍历目录后必须把 Header.Name 改成相对路径。
这一区分很重要:模式来自源文件的 FileMode,路径来自归档策略。不要把源目录的绝对路径直接放入 Header.Name,也不要用字符串替换猜测目录层级。
用 WalkDir 写一个保留模式的打包函数
package main
import (
"archive/tar"
"io"
"os"
"path/filepath"
)
// archiveDir 把 src 的内容写入 dst;归档内路径统一从 src 根目录计算。
func archiveDir(src, dst string) (err error) {
out, err := os.Create(dst)
if err != nil {
return err
}
defer func() {
// 让调用方同时拿到遍历错误和关闭输出文件的错误。
if closeErr := out.Close(); err == nil {
err = closeErr
}
}()
tw := tar.NewWriter(out)
defer func() {
// Close 会写出 tar 的结束块,不能只关闭底层文件。
if closeErr := tw.Close(); err == nil {
err = closeErr
}
}()
return filepath.WalkDir(src, func(path string, d os.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr // 访问失败时停止,避免生成不完整归档。
}
info, err := d.Info()
if err != nil {
return err
}
rel, err := filepath.Rel(src, path)
if err != nil {
return err
}
if rel == "." {
rel = filepath.Base(src) // 保留根目录条目,便于解包后还原层级。
}
link := ""
if d.Type()&os.ModeSymlink != 0 {
link, err = os.Readlink(path) // 只保存链接目标,不跟随链接。
if err != nil {
return err
}
}
hdr, err := tar.FileInfoHeader(info, link)
if err != nil {
return err
}
hdr.Name = filepath.ToSlash(rel) // tar 路径使用正斜杠,避免跨平台差异。
if err := tw.WriteHeader(hdr); err != nil {
return err
}
if !info.Mode().IsRegular() {
return nil // 目录、链接等只需头部;不要把它们当普通文件读取。
}
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close() // 当前回调结束后释放文件描述符。
_, err = io.Copy(tw, file)
return err
})
}

这段实现里,FileInfoHeader 负责把 info.Mode() 反映到 Header.Mode,所以普通文件和目录的权限位会进入 tar 头部。Header.Name 则由 Rel 计算,最后用 ToSlash 统一成归档格式。根目录是否保留是策略问题;上例保留它,解包后更容易得到明确的顶层目录。
三种文件类型对应三种写法
| 类型 | 头部字段 | 正文处理 |
|---|---|---|
| 普通文件 | Mode、Size | 写头后用 io.Copy 复制内容 |
| 目录 | ModeDir、名称末尾的 / | 只写头,不复制正文 |
| 符号链接 | ModeSymlink、Linkname | 只保存链接目标,不读取目标文件 |
目录和符号链接都不能按普通文件调用 io.Copy。另外,WalkDir 默认不跟随符号链接,这正好避免把链接指向的目录递归打进包里。若业务明确需要特殊文件、硬链接或扩展属性,应单独设计归档策略,不能假设 FileInfoHeader 自动覆盖所有系统语义。

模式保留的边界和收尾检查
“保留模式”指 tar 头部记录了源条目的模式,不等于任何解包环境都会得到完全相同的最终权限。解包程序可能受 umask 影响,Windows 等系统也未必按 Unix 权限语义还原。因此发布前应明确目标平台,并在测试包中检查条目名称、类型、模式和链接目标。
还要特别注意两个边界:第一,输出文件不要放在正在遍历的 src 目录里,否则它可能被自己再次打包;第二,tar.Writer.Close 必须先于底层输出文件关闭,才能写完归档结束块。若要生成 .tar.gz,只需在 tar.NewWriter 外再包一层 gzip writer,但关闭顺序要从内到外完成。
相关问题
为什么 Header.Name 不能直接使用 path?因为 path 可能含有绝对路径或本机分隔符;应基于源目录计算相对路径,再用 filepath.ToSlash 转成 tar 习惯的正斜杠。
FileInfoHeader 会自动保存用户和组吗?它能填充部分文件信息,但用户组名称和系统扩展属性是否完整进入归档,取决于 FileInfo 实现与额外字段配置,不能只凭模式位推断。
为什么解包后权限还是变了?归档头部的模式只是输入,解包工具、umask、目标操作系统和安全策略都可能改变最终权限;应检查解包环境,而不是重复修改打包循环。
-
860 收藏
-
843 收藏
-
826 收藏
-
809 收藏
-
792 收藏
-
487 收藏
-
130 收藏
-
184 收藏
-
245 收藏
-
394 收藏
-
274 收藏
-
351 收藏
-
321 收藏
-
266 收藏
-
235 收藏
-
485 收藏
-
266 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习