备份前须检查文件占用:Windows用handle.exe,Linux/macOS用fuser或/proc/pid/fd;推荐os.O_RDONLY|os.O_EXCL尝试打开并重试1–2秒,或依赖上游.touch .ready标记;filepath.WalkDir比Walk高效30%+,需防符号链接循环和路径归一化;增量备份应结合ModTime与Size双重筛选后再哈希;压缩与加密须分离,ZIP禁用压缩,密钥从环境变量读取。

备份前必须检查文件是否被其他进程占用
Go 的 os.OpenFile 或 io.Copy 在源文件正被写入时可能读到不完整内容,尤其日志类文件。不能只依赖 os.Stat 判断存在性,要加锁或用平台级检查。
Windows 下可用 handle.exe(Sysinternals 工具)配合 exec.Command 检查句柄;Linux/macOS 可读取 /proc/[pid]/fd/ 或用 fuser -v [file]。更轻量的做法是尝试以 os.O_RDONLY | os.O_EXCL 打开——虽然该 flag 对普通文件无效,但对某些挂载点或 NFS 有作用,可作为辅助信号。
- 生产环境建议加 1–2 秒重试(最多 3 次),避免因瞬时写入阻塞整个备份流程
- 若业务允许,让上游模块在写完后 touch 一个
.ready文件,备份程序只处理带该标记的文件
用 filepath.WalkDir 而不是 filepath.Walk
filepath.Walk 是基于 os.FileInfo 的递归,每次调用都触发 stat 系统调用,且无法跳过子目录;filepath.WalkDir 使用 fs.DirEntry,一次读取目录项即可获取 name/type,性能高 30%+,还能在回调中返回 filepath.SkipDir 提前终止遍历。
注意:它不会自动处理符号链接循环,需自己记录已访问的 inode(sys.UnixStat().Ino)防死循环;也不解析相对路径中的 ..,若输入路径含 ../logs,需先用 filepath.Abs 归一化。
立即学习“go语言免费学习笔记(深入)”;
- 慎用
filepath.SkipDir:返回它会跳过当前目录下所有子项,但不会影响同级其他目录 - 如果要排除
.git、node_modules,在回调里匹配entry.Name()即可,别拼完整路径再strings.Contains
增量备份必须靠文件修改时间 + 大小双重判断
仅比对 ModTime() 不可靠:NFS 时钟不同步、手动 touch、编辑器“保存即替换”等行为都会导致时间戳变化但内容未变;仅比对大小更糟——两个不同文件可能大小相同。
真正可行的轻量方案是:先筛出 ModTime 新于上次备份时间、且大小不等于历史记录的文件,再对这些候选文件计算 xxhash.Sum64(比 md5 快 5 倍,碰撞率够用)。首次备份可跳过哈希,后续只对筛选后的少量文件计算。
- 哈希值不要存成独立文件,而是写入 SQLite 的
backup_manifest.db表,字段包括:path TEXT, size INTEGER, mtime INTEGER, hash BLOB, backup_time INTEGER - SQLite 的 WAL 模式 +
PRAGMA synchronous = NORMAL能扛住突然断电,避免 manifest 损坏导致全量重备
压缩与加密必须分离处理,别用 archive/zip 直接套 crypto/aes
archive/zip 内置的 ZIP 2.0 加密(SetPassword)是弱算法(CRC + RC2),且 Go 标准库不支持解密;而自己把 *zip.Writer 包进 aes.NewGCM 的 io.WriteCloser 会导致压缩流被截断——因为 zip 需要回写文件头,而加密流无法随机写。
正确顺序是:先写原始 zip 文件 → 关闭 → 用 os.Open 读取 → 流式 AES-GCM 加密 → 写出 .zip.aes。中间不落地可改用 bytes.Buffer,但超过 100MB 就得切分,否则 OOM。
- 加密密钥别硬编码,从环境变量读
BACKUP_KEY,用base64.StdEncoding.DecodeString解码为 32 字节 - ZIP 中禁用压缩(
zip.FileHeader.Method = zip.Store):CPU 比磁盘慢,且加密后压缩率极低,纯增加延迟
syscall.Statfs 检查目标路径剩余空间,并预留 20% 缓冲。


















