第 3 章:让数据留下来——JSON 存储与安全替换
实现文件读取、JSON 编解码和临时文件替换,理解一个小项目也应该认真处理的数据安全问题。
项目路线Go 本地待办清单 CLI3 / 5
为什么第一版选择 JSON
数据库擅长并发、查询和约束,但它会引入安装、连接和迁移。这个项目只有一个本地用户,任务数量也很少,因此 JSON 更符合当前问题规模。
选择简单方案不等于随便写。我们仍然要区分“文件不存在”和“文件已经损坏”:前者代表第一次运行,后者代表数据异常。
读取文件
func (store Store) Load() ([]Task, error) {
body, err := os.ReadFile(store.Path)
if errors.Is(err, os.ErrNotExist) {
return []Task{}, nil
}
if err != nil {
return nil, fmt.Errorf("读取任务文件: %w", err)
}
if len(body) == 0 {
return []Task{}, nil
}
var tasks []Task
if err = json.Unmarshal(body, &tasks); err != nil {
return nil, fmt.Errorf("解析任务文件: %w", err)
}
return tasks, nil
}
注意损坏 JSON 会直接返回错误。程序不会假装任务为空,更不会在下一次保存时覆盖原文件。
不要直接覆盖旧数据
如果使用 os.WriteFile(store.Path, body, 0o600),写到一半断电或磁盘空间耗尽,旧文件可能已经被截断。更稳妥的流程是:
- 在目标目录创建临时文件。
- 写入完整 JSON。
- 调用
Sync请求同步落盘。 - 关闭临时文件。
- 用临时文件替换目标文件。
核心代码如下:
temporary, err := os.CreateTemp(directory, ".tasks-*.tmp")
if err != nil {
return fmt.Errorf("创建临时文件: %w", err)
}
temporaryPath := temporary.Name()
defer os.Remove(temporaryPath)
if _, err = temporary.Write(body); err != nil {
temporary.Close()
return fmt.Errorf("写入临时文件: %w", err)
}
if err = temporary.Sync(); err != nil {
temporary.Close()
return fmt.Errorf("同步临时文件: %w", err)
}
示例为了兼容 Windows,在重命名前会移除旧目标文件。这不是面向高并发场景的事务存储,但比直接边截断边写入更安全,也让失败阶段更清晰。
数据文件放在哪里
默认路径是:
~/.xiaoai-todo/tasks.json
测试和临时体验可以覆盖:
$env:TODO_DATA_FILE="$PWD/test-tasks.json"
go run . add "验证 JSON 存储"
环境变量把“程序行为”和“运行位置”分开,不必为了测试修改源码。
本章验收
连续执行两次 add,关闭程序后运行 list。任务仍然存在,并且数据文件是可读的格式化 JSON。手动把文件改成错误内容后,程序应提示“解析任务文件”,而不是返回空列表。
JARVIS · 当前文章
有哪里没看懂?可以只问这篇。
Jarvis 会限定在《第 3 章:让数据留下来——JSON 存储与安全替换》及其公开关联内容中检索,并把引用定位回原文章节。