因为直接覆盖写入易导致数据丢失或损坏,应采用“读取→修改→写临时文件→原子重命名”流程,并处理文件不存在、json tag缺失、id生成冲突、持久化遗漏等关键问题。

为什么不用 json.Marshal 直接写文件就完事?
因为会丢数据。Go 的 os.WriteFile 或 io.WriteString 是覆盖写入,而博客系统常需「追加新文章」或「更新某篇」,直接全量序列化整个结构体再覆盖,容易在并发写或程序崩溃时导致整个 posts.json 文件变空或损坏。
更稳妥的做法是:读取现有 JSON → 解析为 Go 结构 → 修改内存中数据 → 全量重写文件(但加临时文件 + 原子重命名)。
- 用
os.Rename替代直接覆盖,避免写到一半出错留脏文件 - 临时文件路径必须和目标在同一磁盘分区,否则
Rename会失败(跨分区实际是复制+删除) - 记得设文件权限,比如
0644,否则 Linux 下可能无法被 Web 服务读取
如何安全地从 posts.json 加载并反序列化?
常见错误是忽略 os.IsNotExist,一启动就读不到文件就 panic。博客首次运行时 posts.json 根本不存在,应该默认初始化为空切片,而不是报错退出。
另一个坑是结构体字段没加 JSON tag,导致 json.Unmarshal 解出来全是零值。比如 ID 字段没写 json:"id",反序列化后永远是 0。
立即学习“go语言免费学习笔记(深入)”;
- 加载前先
os.Stat检查文件是否存在,不存在就跳过读取,直接用空切片 - 所有导出字段必须带小写 JSON tag:
json:"title"、json:"created_at" - 时间字段推荐用
time.Time+ 自定义UnmarshalJSON方法,避免字符串格式不一致(如"2024-05-01"vs"2024-05-01T12:00:00Z")
新增文章时,ID 怎么生成才不会重复?
别用 rand.Intn 或时间戳拼接——本地 JSON 文件没有全局锁,多请求并发写入时 ID 极易撞车。简单可靠的方案是:加载全部已有文章,取最大 ID + 1。
这看起来低效,但对「简易博客」完全够用;真到几千篇文章时,IO 开销仍远小于引入数据库的复杂度。
- 确保 ID 字段是
int类型,不是string,否则排序和取最大值会出错 - 如果文件为空(首次运行),最大 ID 应为 0,新文章 ID = 1
- 不要在内存里维护一个全局计数器变量,进程重启就丢了,且多实例部署时彻底失效
修改或删除文章后,为什么前端看不到变化?
大概率是没重新序列化回文件,或者写了但没调 os.Rename 把临时文件顶替原文件。还有一种隐蔽情况:JSON 写入时用了 json.MarshalIndent 带缩进,但某些旧版工具或脚本对空白敏感,误判为格式异常。
另外注意:HTTP handler 里修改了内存数据,但忘记调用持久化函数(比如叫 savePostsToFile),这是最常漏掉的一行代码。
- 每次增删改后,必须显式调用保存函数,不能只改
[]Post变量 - 保存函数内部要捕获
json.Marshal错误(比如字段含不可序列化的 chan/map) - 临时文件名建议用
fmt.Sprintf("posts.json.%d.tmp", time.Now().UnixNano()),避免并发时重名
JSON 文件存储本身不难,难的是在无锁、无事务、无原子写支持的前提下,把「读—改—写」三步做成可靠操作。每一步的异常分支都得想清楚,比如磁盘满、权限不足、文件被占用——这些在本地开发时几乎碰不到,上线后第一个用户发文章就卡住。










