可靠增量备份需规避modtime/size误判,应结合内容哈希与元数据;优先用filepath.walkdir,跳过非普通文件,临时文件+原子重命名写入,并持久化哈希缓存。

为什么 os.Stat 和 os.ReadDir 不能直接用于可靠增量判断
单纯比对文件修改时间(ModTime())或大小(Size())在多数生产场景下会出错:NFS 挂载可能丢失纳秒精度,ext4 默认只记录秒级时间戳,而某些备份工具(如 rsync)或编辑器(如 vim)会先写临时文件再原子重命名,导致 ModTime 被重置。更稳妥的方式是结合内容哈希与元数据——但全量计算 SHA256 太慢,所以得用分块哈希(类似 rsync 的 rolling hash)或跳过已知未变文件。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 优先使用
filepath.WalkDir(Go 1.16+),它不读取子目录内容,比filepath.Walk更轻量且支持跳过目录 - 对每个文件,先检查目标路径是否存在且
os.SameFile(src, dst) == true—— 避免硬链接被误判为“需复制” - 若目标存在,用
os.Stat比对Size()和ModTime().UnixNano();两者都一致时,可跳过哈希计算(约 80% 场景适用) - 仅当大小或时间不一致时,才对文件前 1MB(或自定义阈值)做
sha256.Sum256快速校验——大文件不必全读
如何用 io.Copy + os.Create 实现带进度与原子性的备份写入
直接 os.WriteFile 不安全:写入中途崩溃会导致目标文件损坏;而覆盖旧文件会破坏“增量”语义(旧版本丢失)。正确做法是写入临时文件 + 原子重命名。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 用
dstPath + ".tmp"或filepath.Join(filepath.Dir(dstPath), "."+filepath.Base(dstPath)+".tmp")构造临时路径 - 打开目标目录的父目录 fd(
os.Open(filepath.Dir(dstPath))),再用fd.Sync()确保目录项落盘——这是os.Rename原子性的前提 - 写入时用
io.Copy替代io.ReadAll+Write,避免内存爆涨;可插入进度回调(例如每 1MB 调用一次函数) - 写完后调用
os.Chmod(tmpPath, srcInfo.Mode())保留权限,再os.Rename(tmpPath, dstPath)
如何识别并跳过软链接、设备文件等非普通文件
增量备份中若不加过滤,/dev/sda、/proc/cpuinfo 或循环软链接都可能触发 panic 或无限遍历。Go 的 fs.FileInfo 提供了明确类型判断接口。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 在
filepath.WalkDir的回调中,用entry.Type()判断:entry.Type()&os.ModeSymlink != 0跳过软链接;entry.Type()&os.ModeDevice != 0跳过块/字符设备 - 对符号链接,可用
os.Readlink(path)获取目标,但不要自动跟随——是否跟随应由用户配置决定(如--follow-symlinks) - 用
entry.Type()&os.ModeDir == 0快速排除目录,只处理普通文件(ModeRegular) - Windows 下注意
os.ModeNamedPipe和os.ModeSocket不存在,需用runtime.GOOS分支处理
为什么 sync.Map 不适合做文件哈希缓存
增量备份常需跨多次运行复用哈希结果(比如每天只备份变化文件),此时需要持久化缓存。有人试图用 sync.Map 存上次的 map[string]sha256.Sum256,但这无法解决进程重启后丢失的问题,且并发写入时 key 冲突难调试。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 缓存必须落盘:推荐用 SQLite(单文件、零配置)或简单 JSON 文件(
map[string]struct{ Size int64; ModTime int64; Hash [32]byte }) - JSON 缓存要加文件锁(
flockon Linux/macOS,syscall.LockFileExon Windows),否则多实例同时备份会覆盖彼此记录 - 缓存键建议用
filepath.Clean(srcPath)+filepath.Base(srcPath)组合,避免相对路径歧义 - 每次启动时检查缓存文件是否比源目录更旧(
os.Stat(cachePath).ModTime().Before(dirModTime)),过期则清空重算
真正麻烦的不是怎么算哈希,而是怎么让两次运行之间“记得住”哪些文件没变——这个状态管理比备份逻辑本身更易出错。










