os.WriteFile不能存任务进度,因其覆盖写、无校验、不保证落盘,进程崩溃时文件可能只剩前半段;必须用“临时文件+Sync+Rename”三步闭环实现原子写入。

直接把进度写进文件或数据库,不加校验和原子保护,重启后大概率读到半截数据或丢状态。必须用“临时文件 + Sync + Rename”三步闭环,再配合唯一任务ID和时间戳做幂等判断。
为什么 os.WriteFile 不能存任务进度
它覆盖写、无校验、不保证落盘,进程崩溃时文件可能只剩前半段——比如一个 progress.json 写到一半断电,下次读出来是 invalid JSON 或 missing fields。
-
os.WriteFile是原子的「系统调用层面」,但不是「业务逻辑层面」:它只保证单次写入不被中断,不保证内容完整可解析 - 没有
Sync()调用,数据卡在 OS 缓存里,断电即丢 - 并发写同一文件时,后写入的直接覆盖前一个,进度回滚成旧值
- JSON 格式一旦损坏,
json.Unmarshal直接 panic,没 fallback 机制
用临时文件 + Sync + Rename 实现安全写入
这是 Go 文件持久化最可靠的基础模式,适用于任何结构化状态(进度、配置、快照)。
- 先写入带随机后缀的临时文件,如
progress_123456789.json.tmp - 写完立即调用
f.Sync(),确保数据刷到磁盘 - 最后用
os.Rename()替换原文件——该操作在大多数文件系统上是原子的 - 读取时,只认
progress.json,忽略所有.tmp文件;启动时若发现残留.tmp,按需清理或恢复
示例关键片段:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
func saveProgress(p Progress, path string) error {
tmpPath := path + ".tmp"
f, err := os.Create(tmpPath)
if err != nil {
return err
}
defer f.Close()
enc := json.NewEncoder(f)
if err := enc.Encode(p); err != nil {
return err
}
if err := f.Sync(); err != nil { // 这行不能少
return err
}
return os.Rename(tmpPath, path) // 原子替换
}
如何避免重复恢复或状态错乱
单纯存进度数字不够,得带上上下文标识——否则多个任务共用一个文件,或升级后字段语义变了,旧数据会误解释。
- 每个任务用独立文件,文件名含唯一 ID:
task_abc123_progress.json - 结构体中嵌入版本号字段,如
Version int `json:"v"`,反序列化前校验 - 写入时记录
UpdatedAt time.Time,重启后若发现状态时间早于任务创建时间,视为失效 - 对关键字段(如
CurrentStep)加范围检查,超出枚举值就重置为初始态,不 panic
大任务分阶段写状态,别等全部做完才落盘
上传 10GB 文件时,如果只在最后写一次 "done": true,中间崩溃就全白干。应该按阶段写,且每次写都走原子流程。
- 阶段命名要明确:
"uploading"→"verifying"→"indexing"→"done" - 每阶段完成立刻保存,哪怕只是
{"step": "verifying", "bytes": 2147483648} - 避免写超长字符串(如 base64 日志),改用固定字段+外置日志文件
- 高频更新场景(如秒级进度),可加内存缓冲:每 5 秒或每 10% 进度刷一次盘,减少 IO 压力
真正难的不是怎么存,而是怎么定义「状态」本身——字段是否可逆、是否支持跳过、失败后从哪恢复。这些逻辑必须在设计阶段就固化进结构体,而不是靠后期补救。

















