os.Copy 无法实现增量同步,因其不比对、不校验、全量重传;需依赖变化检测,而标准库缺乏差异计算与分块哈希能力,仅靠修改时间和大小易误判,可靠方案为内容哈希或分块哈希。

为什么直接用 os.Copy 做不了增量同步
因为 os.Copy 只管把源文件全量写过去,不比对、不跳过、不校验。哪怕只改了一个字节,它也会重传整个文件——网络和磁盘开销都浪费在重复数据上。真正的增量同步必须依赖「变化检测」,而 Go 标准库本身不提供文件差异计算或块级哈希功能。
常见错误现象:syncFile() 逻辑里反复调用 os.Copy,结果发现备份耗时没变、流量没省、磁盘空间反而多占一份。
- 判断依据只能是:修改时间(
FileInfo.ModTime())+ 文件大小(FileInfo.Size()),但这两项都可能被篡改或不精确(如 FAT32 时间精度为 2 秒) - 可靠方式是内容哈希(如
sha256),但大文件全量计算太慢;更实用的是分块哈希(类似 rsync 的 rolling checksum) - Go 生态中
github.com/pkg/sftp和github.com/ncw/rclone底层用了分块算法,但直接集成复杂;轻量方案推荐用github.com/kevinburke/ssh-config配合外部rsync调用
用 os.Stat + crypto/sha256 实现简易增量判断
适合中小文件(
注意:不要在每次备份都全量算哈希,应缓存上次哈希值(比如写进一个 .backup-meta.json 文件)。
立即学习“go语言免费学习笔记(深入)”;
- 计算哈希时务必用
io.Copy流式读取,避免os.ReadFile把大文件全加载进内存 - 对比前先检查
os.IsNotExist(err),否则目标缺失时会 panic - Windows 下注意路径分隔符:用
filepath.Join而不是硬拼"\"或"/"
h := sha256.New() f, _ := os.Open(srcPath) defer f.Close() io.Copy(h, f) sum := h.Sum(nil)
如何安全替换目标文件而不中断读取
直接 os.WriteFile(dstPath, data, 0644) 有风险:写到一半失败,目标文件就损坏了。生产环境必须用原子写入。
在 Golang 中使用 samber/hot 进行内存缓存,支持 LRU、LFU、TinyLFU、W‑TinyLFU、S3FIFO、ARC、TwoQueue、SIEVE、FIFO 等淘汰算法,提供 TTL、缓存加载器及分片功能。
正确做法是写临时文件 + os.Rename。但要注意:os.Rename 在跨分区时会失败(返回 syscall.EXDEV),此时需 fallback 到 copy + remove。
- 临时文件名建议用
dstPath + ".tmp" + randomString(6),避免冲突 - 写完后立即
os.Chmod(tmpPath, fi.Mode()),保留原始权限 - 替换前先
os.Chtimes(tmpPath, srcModTime, srcModTime)同步时间戳
增量同步绕不开的三个状态管理细节
增量 ≠ 只看当前文件。你得知道「上次备份到哪了」「哪些文件删了」「哪些目录结构变了」。这些信息不能靠猜,得持久化。
最容易被忽略的是删除同步:源端删了一个文件,目标端如果不删,就会越积越多。但又不能简单遍历目标目录删所有不在源里的文件——万一源临时离线呢?
- 必须维护一个本地快照(snapshot),记录上次成功备份时每个文件的
path + hash + modtime - 每次同步前,先生成当前源的快照,再和旧快照 diff:新增/变更/删除三类动作分别处理
- 快照格式推荐 JSON(便于人工检查),但字段要包含
version字段,方便未来升级结构
真正复杂的点不在代码行数,而在状态一致性:比如备份中途断电,快照写了一半,下次该信哪个文件?这类边界情况没有银弹,只能靠写前 fsync + 写后校验来压低概率。

















