Rust使用serde_json写入配置文件时末尾多出额外'}'的问题排查
问题根因
额外}出现的核心原因是文件写入逻辑没有截断旧内容:
你打开文件时使用的参数是.read(true).write(true).create(true),该模式打开已有文件时,文件指针默认停在文件起始位置,write操作只会从头开始覆盖对应长度的内容,不会自动清除文件末尾未被新内容覆盖的旧数据。
举个对应场景的例子:
- 某次序列化生成的JSON总长度为100字节,写入后文件长度为100字节,内容合法
- 后续配置字段值变化(比如数值从3位的
200变成2位的20),新序列化的JSON总长度变为99字节,写入时只会覆盖前99字节,原文件第100位的}会被保留,最终就出现末尾多一个}的非法格式
你换成to_writer接口后问题加重也是同一个原因:to_writer本身只负责向当前文件指针位置写入序列化内容,不会主动截断文件清空旧数据。
除此之外你的代码还有几个容易引发异常的隐患: - 写入操作非原子:写入过程中如果进程崩溃、设备掉电,文件会停留在半写状态,直接损坏无法解析
- 读文件时忽略了
read_to_string的返回结果,遇到IO错误时会用空字符串/不完整字符串做反序列化,触发无意义的解析报错 - 写入完成后没有强制刷盘,极端情况下内核页缓存未落地就掉电会丢失最新配置
修复方案
最小改动修复
给文件打开参数加上.truncate(true),打开文件时直接清空原有旧内容,同时补全错误处理、写入后刷盘逻辑:
use std::fs::OpenOptions; use std::io::{Read, Write}; use serde_json::{to_string, from_str}; // 其余依赖(日志宏、Struct_object定义、file_exist方法等)保持原有引入即可 pub fn write_config(data: &Struct_object) { let mut refval = data.clone(); match to_string(&mut refval) { Ok(content) => { let file_result = OpenOptions::new() .write(true) .create(true) .truncate(true) // 核心修复:打开文件时清空所有旧内容 .open("config.json"); match file_result { Ok(mut file) => { // 用write_all确保完整写入所有字节,避免部分写入 if let Err(e) = file.write_all(content.as_bytes()) { warn!("Config.json 写入失败, data: {{ {} }}, 错误: {{ {} }}", data, e); return; } // 强制刷盘确保内容落地到磁盘 if let Err(e) = file.sync_all() { warn!("Config.json 刷盘失败, 错误: {{ {} }}", e); } } Err(e) => { warn!("Config.json 打开失败, data: {{ {} }}, 错误: {{ {} }}", data, e); } } } Err(e) => warn!("Config.json 序列化失败, data: {{ {} }}, 错误: {{ {} }}", data, e), } } pub fn read_config() -> Struct_object { if !file_exist() { return Struct_object::default(); } let file_result = OpenOptions::new().read(true).open("config.json"); let mut buf = String::new(); match file_result { Ok(mut file) => { // 处理读IO错误,不要忽略返回值 if let Err(e) = file.read_to_string(&mut buf) { warn!("config.json 读取失败,加载默认配置, 错误: {}", e); return Struct_object::default(); } match from_str::<Struct_object>(&buf) { Ok(config) => config, Err(e) => { warn!("config.json 解析失败,加载默认配置, 错误: {}", e); Struct_object::default() } } } Err(e) => { warn!("config.json 打开失败,加载默认配置, 错误: {}", e); Struct_object::default() } } }
更稳妥的原子写方案
如果要彻底避免半写损坏问题,可以采用临时文件替换的逻辑:先把内容写入同目录下的临时文件,刷盘确认后通过rename原子替换正式配置文件,同文件系统下的rename操作是原子的,永远不会出现半写的坏文件:
use std::fs; use std::fs::OpenOptions; use std::io::Write; use serde_json::to_string; pub fn write_config(data: &Struct_object) { let mut refval = data.clone(); let content = match to_string(&mut refval) { Ok(c) => c, Err(e) => { warn!("配置序列化失败, 错误: {}", e); return; } }; const TMP_PATH: &str = "config.json.tmp"; // 打开临时文件时同样清空旧内容 let mut tmp_file = match OpenOptions::new() .write(true) .create(true) .truncate(true) .open(TMP_PATH) { Ok(f) => f, Err(e) => { warn!("临时配置文件打开失败, 错误: {}", e); return; } }; if let Err(e) = tmp_file.write_all(content.as_bytes()) { warn!("临时配置文件写入失败, 错误: {}", e); return; } if let Err(e) = tmp_file.sync_all() { warn!("临时配置文件刷盘失败, 错误: {}", e); return; } // 原子替换正式文件 if let Err(e) = fs::rename(TMP_PATH, "config.json") { warn!("配置文件替换失败, 错误: {}", e); } }
内容的提问来源于stack exchange,提问作者Dykeiichi
相关产品推荐
相关产品推荐

