你写的代码真的可靠吗?一个看似简单的文件同步工具,可能在静默中吃掉用户的数据,而用户连恢复的机会都没有。我曾动手写了一个小工具,在远程项目和本地文件夹之间搬运文件,类似 git 的 clone、pull、push、status。只要代码一跨网络触碰别人的文件,就继承了一类比崩溃更可怕的 bug:悄然毁灭用户无法找回的工作成果。

下面是我踩过(或差点踩进)的三个数据丢失陷阱,每一个都能轻松通过朴素测试套件,但真实运行时就是灾难。附上修复方法,亲测有效。

1. os.WriteFile 先清空再写入,中断就只剩残骸

最直接的下载保存方式是这样:os.WriteFile(path, data, 0o644)。但 O_TRUNC 标志会先清空文件再写入。如果进程在这两步之间被杀死——Ctrl-C、内存溢出、合上笔记本盖子——留下的就是一个新字节的前缀,旧内容荡然无存。用户只得到半个文件。

修复手法和数据库一样:在目标文件旁边写临时文件,然后做原子 rename。tmp, _ := os.CreateTemp(filepath.Dir(path), “.tmp-*”);写入数据;tmp.Close();最后 os.Rename(tmp.Name(), path)。在同一文件系统内,rename(2) 是原子的,任何读到该路径的人看到的要么是完整的旧文件,要么是完整的新文件,绝不会是碎片。两个容易踩的坑:临时文件必须放在目标文件同一目录下(放进 /tmp 会跨设备链接报 EXDEV),此外 rename 会交换 inode,目标文件上的硬链接和扩展属性不会保留——这是需要接受的代价。

更阴险的连锁伤害是:如果写入发生在本机账本记录之前,被中断的写入留下的碎片,下次运行工具时会检测到“本地文件变更”,然后提示用户用 --force 覆盖。你损坏了文件,然后把锅甩给用户,再递上一个破坏性参数当解药。原子写直接从根源掐掉这一整条链。

2. 只靠时间戳或 etag 做冲突检测,会漏掉唯一导致数据丢失的状況

一个诱人的廉价冲突检查:比较服务器的 etag(或修改时间)和你上次保存的值,不一样就重新下载。这恰好漏掉了唯一真正丢失数据的情况:两方都发生了修改。用户在本地编辑过,同一时间服务器上的文件也变了,etag 比较只看到“服务端有差异”,于是高高兴兴地覆盖掉本地编辑。一去不复返。

冲突决策必须基于字节内容,而不是元数据:

  • 本地哈希 == 上次同步哈希 → 干净,直接取远程版本。
  • 本地哈希 ≠ 上次同步哈希,且远程已变 → 冲突,什么都不碰。
  • 只有一側有变化 → 应用那側的版本。

元数据可以是跳过工作的快速通道,但绝不能成为授权覆盖的依据。授权的唯一裁判是哈希。

3. “删掉服务端没有的”会把你根本没同步过的东西一起删掉

--prune 或镜像标志是同步工具造成最严重伤害的地方,因为删除操作没有撤销余地。朴素的规则——“本地有但服务端没有的东西就删掉”——会残忍地删掉那些你从未通过工具同步过的文件。用户可能正好在本地目录里放了其他项目、私密笔记,只因和同步根目录混在一起,就在一次镜像操作中灰飞烟灭。

这个陷阱的可怕之处在于它不只影响当前同步数据,还会把范围外的内容当作垃圾清理。修复思路必须严格限定删除对象的来源:只删除由本次同步写下的文件,或者维护一个由工具自己掌握的文件清单来判定哪些是“受管”的。如果一开始没有这样的追踪机制,请立即关闭自动镜像选项,千万别让一次“整洁”的操作变成无法挽回的灾祸。