You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 22:09:21