应优先用 os.samefile 判断是否同一文件,再比大小,大小不等则直接返回 false;大小相等且文件超 1mb 时,用 xxhash.sum64 对首尾各 3 块、中间随机 2 块(64kb/块)抽样校验,任一哈希不同即返回 false,全相同则视为可信。

为什么 os.Stat + os.ReadFile 不适合大文件比对
直接读全量内容再 bytes.Equal,内存和 IO 开销都不可控。100MB 文件会一次性分配等大内存,还可能触发 GC 压力;更糟的是,哪怕只有末尾 1 字节不同,也得读完全部才敢下结论。
真正要效仿 rsync 的思路,核心是「分块校验 + 早期退出」:先比大小,再比哈希(如 xxhash.Sum64),只在哈希冲突时才逐块比对字节。
- 大小不等 → 直接判定不同,
return false, nil - 大小相等但文件超过 1MB → 计算固定块(如 64KB)的
xxhash.Sum64,首尾各取 3 块,中间随机抽 2 块 - 所有抽样块哈希一致 → 大概率相同,可跳过全量比对(设为可信阈值)
- 任一哈希不等 → 立即返回
false,不继续
用 io.SectionReader 安全读取任意块,避免内存爆炸
os.ReadFile 是方便,但没法控制读哪一段;而 io.CopyN 或 io.ReadFull 配合 os.Open + Seek 又容易出错(比如未处理 io.EOF 或偏移越界)。
io.SectionReader 是标准库里最稳妥的选择:它包装一个 *os.File,限定读取范围,且不会移动原文件指针,也不会多读 —— 即使你指定长度超过文件剩余字节,它也只返回实际可读部分 + io.EOF。
立即学习“go语言免费学习笔记(深入)”;
- 创建方式:
sr := io.NewSectionReader(f, offset, length) - 读哈希块时,用
xxhash.New()+io.Copy就行,不用管缓冲区管理 - 注意:
offset + length超过文件大小时,SectionReader自动截断,不会 panic
rsync 的滚动哈希没在 Go 标准库,别硬套 adler32
有人看到 rsync 用滚动哈希(rolling hash)就去翻 hash/adler32,但 adler32 在 Go 里是完整哈希,不支持增量更新;而且它碰撞率高、不适合小块校验。真要滚动,得自己实现或用第三方如 github.com/minio/sha256-simd(但它也不滚动)。
实际工程中,**用固定块 + 快速非加密哈希(如 xxhash)+ 抽样策略,效果和复杂滚动哈希差不多,还更可控**。
- 引入:
go get github.com/cespare/xxhash/v2 - 单块哈希:用
xxhash.Sum64(),比md5快 10 倍以上,且无密码学开销 - 不要试图在 Go 里手写 rsync 风格的滑动窗口 —— 没必要,Go 的并发模型更适合并行抽样块
同步前必须检查 os.SameFile,否则可能自比自
如果源和目标是同一文件(比如硬链接、或路径解析后指向同一个 inode),os.Stat 返回的 dev/inode 相同。此时任何比对都多余,还可能因文件被其他进程写入导致结果不一致。
这个检查极轻量,一行代码就能拦住大量无效操作:
fi1, _ := os.Stat(src)
fi2, _ := os.Stat(dst)
if os.SameFile(fi1, fi2) {
return true, nil
}
漏掉这步,在容器内或 NFS 挂载点上特别容易踩坑 —— 路径不同但 inode 相同,结果反复“同步”同一个文件。
哈希抽样再快,也快不过一次 stat 系统调用。真正难的是把「什么时候该比」「比到哪一层停」想清楚,而不是堆算法。










