登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

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
	})
}
Go WalkDir 到 tar.Writer 的目录打包静态结构说明图
图1:静态说明图展示 WalkDir、相对路径、FileInfoHeader 与 tar.Writer 的职责边界,不是运行截图。

这段实现里,FileInfoHeader 负责把 info.Mode() 反映到 Header.Mode,所以普通文件和目录的权限位会进入 tar 头部。Header.Name 则由 Rel 计算,最后用 ToSlash 统一成归档格式。根目录是否保留是策略问题;上例保留它,解包后更容易得到明确的顶层目录。

三种文件类型对应三种写法

类型头部字段正文处理
普通文件Mode、Size写头后用 io.Copy 复制内容
目录ModeDir、名称末尾的 /只写头,不复制正文
符号链接ModeSymlink、Linkname只保存链接目标,不读取目标文件

目录和符号链接都不能按普通文件调用 io.Copy。另外,WalkDir 默认不跟随符号链接,这正好避免把链接指向的目录递归打进包里。若业务明确需要特殊文件、硬链接或扩展属性,应单独设计归档策略,不能假设 FileInfoHeader 自动覆盖所有系统语义。

Go tar 普通文件目录符号链接与模式字段关系说明图
图2:结构图展示不同文件类型对应的 tar Header 字段;它是原创说明图,不代表实际归档结果。

模式保留的边界和收尾检查

“保留模式”指 tar 头部记录了源条目的模式,不等于任何解包环境都会得到完全相同的最终权限。解包程序可能受 umask 影响,Windows 等系统也未必按 Unix 权限语义还原。因此发布前应明确目标平台,并在测试包中检查条目名称、类型、模式和链接目标。

还要特别注意两个边界:第一,输出文件不要放在正在遍历的 src 目录里,否则它可能被自己再次打包;第二,tar.Writer.Close 必须先于底层输出文件关闭,才能写完归档结束块。若要生成 .tar.gz,只需在 tar.NewWriter 外再包一层 gzip writer,但关闭顺序要从内到外完成。

相关问题

为什么 Header.Name 不能直接使用 path?因为 path 可能含有绝对路径或本机分隔符;应基于源目录计算相对路径,再用 filepath.ToSlash 转成 tar 习惯的正斜杠。

FileInfoHeader 会自动保存用户和组吗?它能填充部分文件信息,但用户组名称和系统扩展属性是否完整进入归档,取决于 FileInfo 实现与额外字段配置,不能只凭模式位推断。

为什么解包后权限还是变了?归档头部的模式只是输入,解包工具、umask、目标操作系统和安全策略都可能改变最终权限;应检查解包环境,而不是重复修改打包循环。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>