应优先用os.SameFile判断是否同一文件,再比大小和修改时间;大小或时间不同则需同步;>1MB时用xxhash.Sum64抽样首尾各3块、中间随机2块(64KB/块),任一哈希不同即需同步,全相同则视为可信。

Go 语言本身不带 rsync 算法实现,但可以复现其核心逻辑:**跳过未变文件 + 分块比对 + 差量传输**。硬套 os.Stat + os.ReadFile 全量比对,会吃光内存、拖垮 IO、错过早期退出机会——这不是增量同步,是“假装增量”的全量搬运。
如何快速判断两个文件是否需要同步
别一上来就读内容。先走轻量路径:
- 用
os.SameFile检查源与目标是否指向同一 inode(比如硬链接或挂载点重叠),是则直接跳过 - 对比
os.Stat返回的Size()和ModTime(),任一不同就标记为需同步 - 若大小和时间戳都一致,且文件 ≤1MB,可直接视为相同(小文件误判成本低)
- 若 >1MB,进入抽样哈希阶段,避免全读
大文件怎么安全抽样比对(不用读全量)
用 io.SectionReader 配合 xxhash.Sum64 抽固定块,不是滚动哈希,也不用 adler32 —— Go 标准库没提供安全滚动接口,硬写易出错。
- 块大小设为 64KB,首尾各取 3 块,中间随机选 2 块(用
rand.Intn控制偏移) - 每块用
xxhash.New()+io.Copy计算哈希,不分配大缓冲区 - 任一抽样块哈希不等,立刻返回 “需同步”,不再继续
- 全部抽样块一致,按阈值(如 99.9% 置信度)视为可信,跳过全量比对
示例关键片段:
立即学习“go语言免费学习笔记(深入)”;
sr := io.NewSectionReader(f, offset, 64*1024) h := xxhash.New() io.Copy(h, sr) sum := h.Sum64()
差量传输时如何避免覆盖失败或数据损坏
同步不是覆盖,是“安全替换”。尤其在目标路径已存在时:
- 永远先写临时文件(如
target.tmp),写完再os.Rename原子替换 - 写临时文件前,检查磁盘剩余空间是否 ≥ 源文件大小(
syscall.Statfs可查) - 若传输中断,残留的
.tmp文件必须被清理,建议用os.Remove+os.IsNotExist容错处理 - 不要用
os.Create直接打开目标路径——万一写到一半崩溃,原文件就丢了
为什么别自己实现 rsync 风格的滚动哈希
rsync 的滑动窗口 + adler32/md5 组合,在 Go 里既难写又没必要。实际工程中:
-
adler32在 Go 的hash/adler32包里是完整哈希,不支持增量 feed,没法做滚动 - 真要滚动,得手写状态机管理窗口、缓存历史块、处理边界,极易引入 off-by-one 或越界 panic
- 抽样固定块 +
xxhash的策略,在真实场景(如代码目录、日志归档)命中率超 95%,且并发友好、无 GC 压力 - 滚动哈希真正价值在服务端预建索引(如 btrfs send/receive),而 Go 同步工具多为点对点直连,客户端算一次就够了
最易被忽略的一点:所有路径操作前,务必调用 filepath.Clean 和 filepath.Abs 归一化,否则 ../、软链、大小写混用会导致 os.SameFile 失效或 stat 错位。


















