不灭的焱

革命尚未成功,同志仍须努力 下载Java21

作者:AlbertWen  添加时间:2026-07-19 21:33:32  修改时间:2026-07-25 15:45:34  分类:01.Rust编程  编辑

目录

仓库地址:https://crates.io/crates/toml

Rust toml 库详细使用教程

toml 是 Rust 生态中常用的 TOML 编码、解码库,支持通过 Serde 在 TOML 文本与 Rust 数据结构之间转换。它的定位类似于 serde_json:负责把配置文本解析成 Rust 对象,也负责把 Rust 对象序列化成 TOML。当前文档对应 1.1.x 版本,并支持 TOML 1.1.0 规范。(Docs.rs)

最常见的使用流程是:

application.toml
        ↓ 读取文件
      String
        ↓ toml::from_str()
   Rust 配置结构体

反过来则是:

Rust 配置结构体
        ↓ toml::to_string_pretty()
      String
        ↓ 写入文件
application.toml

一、创建项目并安装依赖

创建 Rust 2024 项目:

cargo new toml-demo
cd toml-demo

添加依赖:

cargo add toml
cargo add serde --features derive

对应的 Cargo.toml

[package]
name = "toml-demo"
version = "0.1.0"
edition = "2024"

[dependencies]
serde = { version = "1", features = ["derive"] }
toml = "1"

这里推荐写:

toml = "1"

而不是锁死某个补丁版本,例如 1.1.2。这样 Cargo 可以自动使用兼容的 1.x 版本。

toml 默认启用了 parsedisplayserdestd 功能,普通配置文件读写不需要再额外指定 features。(Docs.rs)

二、先了解 TOML 基本语法

1. 基本数据类型

# 字符串
name = "IT工单系统"

# 整数
port = 5800

# 浮点数
version_number = 1.5

# 布尔值
enabled = true

# 数组
features = ["rbac", "ticket", "asset"]

# 日期时间
start_time = 2026-07-19T20:30:00+08:00

TOML 支持字符串、整数、浮点数、布尔值、日期时间、数组、内联表和普通表,并且区分大小写。(toml.io)

对应的 Rust 类型通常为:

TOML 类型 Rust 类型
"hello" String
8080 i64u16u32
3.14 f64
true bool
[1, 2, 3] Vec<i32>
[server] Rust 结构体
[[servers]] Vec<Server>
日期时间 toml::value::Datetime

2. 表 Table

TOML 中的表类似于 JSON 对象或 Rust 结构体:

[server]
host = "0.0.0.0"
port = 5800
workers = 8

可以理解为:

{
  "server": {
    "host": "0.0.0.0",
    "port": 5800,
    "workers": 8
  }
}

3. 嵌套表

[database]
url = "mysql://root:123456@127.0.0.1:3306/workorder"

[database.pool]
min_connections = 5
max_connections = 20

对应的层级结构:

database
├── url
└── pool
    ├── min_connections
    └── max_connections

4. 表数组

当配置中有多个服务器、上游服务或节点时,可以使用 [[名称]]

[[upstreams]]
name = "user-service"
url = "http://127.0.0.1:8101"
timeout_seconds = 5

[[upstreams]]
name = "ticket-service"
url = "http://127.0.0.1:8102"
timeout_seconds = 10

它对应 Rust 中的:

Vec<UpstreamConfig>

每出现一次 [[upstreams]],就会向数组增加一个对象。(toml.io)

5. TOML 没有 null

下面这种写法是错误的:

redis_url = null

TOML 没有 JSON 那样的 null 类型。没有值时,一般直接不写该配置项,然后在 Rust 中使用:

redis_url: Option<String>

TOML 规范要求配置值必须是明确的数据类型,不能只写键而不提供值。(toml.io)

三、使用 toml::Table 动态读取配置

当配置结构不固定,或者只是临时读取几个字段时,可以使用:

toml::Table

toml::Table 本质上是:

Map<String, toml::Value>

toml::Value 可以表示字符串、整数、浮点数、布尔值、日期时间、数组和表。(Docs.rs)

示例

use std::error::Error;
use std::io;
use toml::{Table, Value};

fn main() -> Result<(), Box<dyn Error>> {
    let content = r#"
        title = "IT工单系统"

        [server]
        host = "127.0.0.1"
        port = 5800
        enabled = true
    "#;

    let table: Table = content.parse()?;

    let title = table
        .get("title")
        .and_then(Value::as_str)
        .ok_or_else(|| {
            io::Error::new(
                io::ErrorKind::InvalidData,
                "缺少 title 配置",
            )
        })?;

    let server = table
        .get("server")
        .and_then(Value::as_table)
        .ok_or_else(|| {
            io::Error::new(
                io::ErrorKind::InvalidData,
                "缺少 [server] 配置",
            )
        })?;

    let host = server
        .get("host")
        .and_then(Value::as_str)
        .unwrap_or("0.0.0.0");

    let port = server
        .get("port")
        .and_then(Value::as_integer)
        .unwrap_or(8080);

    let enabled = server
        .get("enabled")
        .and_then(Value::as_bool)
        .unwrap_or(false);

    println!("系统名称:{title}");
    println!("监听地址:{host}:{port}");
    println!("是否启用:{enabled}");

    Ok(())
}

输出:

系统名称:IT工单系统
监听地址:127.0.0.1:5800
是否启用:true

toml::Value 常用方法

value.as_str()
value.as_integer()
value.as_float()
value.as_bool()
value.as_datetime()
value.as_array()
value.as_table()

可变操作对应:

value.as_array_mut()
value.as_table_mut()

所有这些方法返回 Option。类型不匹配时返回 None,而不是直接报错。(Docs.rs)

例如:

let port = server
    .get("port")
    .and_then(Value::as_integer);

假设配置写成:

port = "5800"

因为这里是字符串,不是整数,所以:

Value::as_integer()

会返回:

None

四、动态修改 TOML

use std::error::Error;
use std::io;
use toml::{Table, Value};

fn main() -> Result<(), Box<dyn Error>> {
    let content = r#"
        [server]
        host = "127.0.0.1"
        port = 5800
    "#;

    let mut table: Table = content.parse()?;

    let server = table
        .get_mut("server")
        .and_then(Value::as_table_mut)
        .ok_or_else(|| {
            io::Error::new(
                io::ErrorKind::InvalidData,
                "缺少 [server] 配置",
            )
        })?;

    server.insert(
        "port".to_string(),
        Value::Integer(8080),
    );

    server.insert(
        "workers".to_string(),
        Value::Integer(8),
    );

    let output = toml::to_string_pretty(&table)?;

    println!("{output}");

    Ok(())
}

输出类似:

[server]
host = "127.0.0.1"
port = 8080
workers = 8

五、推荐方式:反序列化为 Rust 结构体

在正式项目中,更推荐把 TOML 直接转换成强类型结构体。

优势是:

  • 字段名称明确;
  • 字段类型明确;
  • 缺少必要字段时直接报错;
  • 字段类型错误时直接报错;
  • IDE 可以自动补全;
  • 业务代码不需要到处调用 get()

核心方法是:

toml::from_str::<目标类型>(&toml字符串)

目标类型必须实现 Serde 的 Deserialize trait。(Docs.rs)

示例一:简单结构体映射

application.toml

name = "IT工单系统"
debug = true

[server]
host = "0.0.0.0"
port = 5800

src/main.rs

use serde::Deserialize;
use std::error::Error;
use std::fs;

#[derive(Debug, Deserialize)]
struct AppConfig {
    name: String,
    debug: bool,
    server: ServerConfig,
}

#[derive(Debug, Deserialize)]
struct ServerConfig {
    host: String,
    port: u16,
}

fn main() -> Result<(), Box<dyn Error>> {
    let content = fs::read_to_string("application.toml")?;

    let config: AppConfig = toml::from_str(&content)?;

    println!("{config:#?}");

    println!(
        "{} 启动地址:http://{}:{}",
        config.name,
        config.server.host,
        config.server.port
    );

    Ok(())
}

输出:

AppConfig {
    name: "IT工单系统",
    debug: true,
    server: ServerConfig {
        host: "0.0.0.0",
        port: 5800,
    },
}

IT工单系统启动地址:http://0.0.0.0:5800

这里的映射关系为:

name            → AppConfig.name
debug           → AppConfig.debug
[server]        → AppConfig.server
server.host     → ServerConfig.host
server.port     → ServerConfig.port

六、完整的 Web 项目配置示例

下面结合 Rust 2024、Salvo、SeaORM、MySQL 和 Redis 的项目场景设计配置。

1. 配置文件

创建:

config/application.toml

内容:

environment = "development"

[application]
name = "workorder-api"
version = "1.0.0"

[server]
bind-address = "0.0.0.0"
port = 5800
workers = 8
request_timeout_seconds = 30
tls_enabled = false

[database]
url = "mysql://root:123456@127.0.0.1:3306/workorder"
min_connections = 5
max_connections = 20
connect_timeout_seconds = 10
sql_logging = true

[redis]
url = "redis://127.0.0.1:6379"
database = 0
pool_size = 10

[auth]
access_token_expire_seconds = 7200
refresh_token_expire_seconds = 604800

[[upstreams]]
name = "user-service"
url = "http://127.0.0.1:8101"
timeout_seconds = 5

[[upstreams]]
name = "message-service"
url = "http://127.0.0.1:8102"
timeout_seconds = 10

2. 配置结构体

创建:

src/config.rs
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AppConfig {
    pub environment: Environment,

    pub application: ApplicationConfig,

    pub server: ServerConfig,

    pub database: DatabaseConfig,

    pub redis: Option<RedisConfig>,

    pub auth: AuthConfig,

    #[serde(default)]
    pub upstreams: Vec<UpstreamConfig>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Environment {
    Development,
    Test,
    Production,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ApplicationConfig {
    pub name: String,
    pub version: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ServerConfig {
    #[serde(rename = "bind-address")]
    pub bind_address: String,

    pub port: u16,

    #[serde(default = "default_workers")]
    pub workers: usize,

    #[serde(default = "default_request_timeout")]
    pub request_timeout_seconds: u64,

    #[serde(default)]
    pub tls_enabled: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DatabaseConfig {
    pub url: String,

    #[serde(default = "default_min_connections")]
    pub min_connections: u32,

    #[serde(default = "default_max_connections")]
    pub max_connections: u32,

    #[serde(default = "default_connect_timeout")]
    pub connect_timeout_seconds: u64,

    #[serde(default)]
    pub sql_logging: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RedisConfig {
    pub url: String,

    #[serde(default)]
    pub database: i64,

    #[serde(default = "default_redis_pool_size")]
    pub pool_size: u32,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AuthConfig {
    pub access_token_expire_seconds: u64,
    pub refresh_token_expire_seconds: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct UpstreamConfig {
    pub name: String,
    pub url: String,

    #[serde(default = "default_upstream_timeout")]
    pub timeout_seconds: u64,
}

fn default_workers() -> usize {
    4
}

fn default_request_timeout() -> u64 {
    30
}

fn default_min_connections() -> u32 {
    5
}

fn default_max_connections() -> u32 {
    20
}

fn default_connect_timeout() -> u64 {
    10
}

fn default_redis_pool_size() -> u32 {
    10
}

fn default_upstream_timeout() -> u64 {
    5
}

3. 加载配置

继续在 src/config.rs 中增加:

use std::error::Error;
use std::fmt;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};

#[derive(Debug)]
pub enum ConfigError {
    Read {
        path: PathBuf,
        source: io::Error,
    },

    Parse {
        path: PathBuf,
        source: toml::de::Error,
    },
}

impl fmt::Display for ConfigError {
    fn fmt(
        &self,
        formatter: &mut fmt::Formatter<'_>,
    ) -> fmt::Result {
        match self {
            Self::Read { path, source } => {
                write!(
                    formatter,
                    "读取配置文件失败:{},原因:{}",
                    path.display(),
                    source
                )
            }

            Self::Parse { path, source } => {
                write!(
                    formatter,
                    "解析配置文件失败:{},原因:{}",
                    path.display(),
                    source
                )
            }
        }
    }
}

impl Error for ConfigError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            Self::Read { source, .. } => Some(source),
            Self::Parse { source, .. } => Some(source),
        }
    }
}

pub fn load_config(
    path: impl AsRef<Path>,
) -> Result<AppConfig, ConfigError> {
    let path = path.as_ref().to_path_buf();

    let content = fs::read_to_string(&path).map_err(|source| {
        ConfigError::Read {
            path: path.clone(),
            source,
        }
    })?;

    toml::from_str(&content).map_err(|source| {
        ConfigError::Parse {
            path,
            source,
        }
    })
}

这里把错误分成了两类:

ConfigError::Read

表示文件不存在、没有权限或读取失败。

ConfigError::Parse

表示文件能读取,但 TOML 格式错误,或者配置内容无法映射到 Rust 结构体。

4. 在 main.rs 中使用

mod config;

use config::load_config;
use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
    let config = load_config("config/application.toml")?;

    println!("应用名称:{}", config.application.name);

    println!(
        "监听地址:{}:{}",
        config.server.bind_address,
        config.server.port
    );

    println!("运行环境:{:?}", config.environment);

    println!("数据库地址:{}", config.database.url);

    if let Some(redis) = &config.redis {
        println!("Redis 地址:{}", redis.url);
    }

    for upstream in &config.upstreams {
        println!(
            "上游服务:{},地址:{}",
            upstream.name,
            upstream.url
        );
    }

    Ok(())
}

七、常用 Serde 属性

toml 通过 Serde 进行结构体转换,因此 Serde 的字段属性非常重要。

1. #[serde(default)]

表示字段不存在时使用类型的默认值。

#[derive(Debug, Deserialize)]
struct ServerConfig {
    #[serde(default)]
    tls_enabled: bool,
}

配置文件没有:

tls_enabled = false

也不会报错,因为:

bool::default()

就是:

false

其他常见默认值:

String::default()  → ""
Vec::default()     → []
bool::default()    → false
整数默认值          → 0
Option::default()  → None

2. 自定义默认值

fn default_port() -> u16 {
    5800
}

#[derive(Debug, Deserialize)]
struct ServerConfig {
    #[serde(default = "default_port")]
    port: u16,
}

当 TOML 中没有 port 时:

config.port == 5800

3. Option<T>

可选配置使用 Option<T>

#[derive(Debug, Deserialize)]
struct AppConfig {
    redis: Option<RedisConfig>,
}

如果配置文件中没有:

[redis]

结果就是:

config.redis == None

如果存在 [redis],则是:

Some(RedisConfig { ... })

4. #[serde(rename = "...")]

Rust 字段名不能包含 -,但 TOML 键可以包含。

TOML:

bind-address = "0.0.0.0"

Rust:

#[derive(Debug, Deserialize)]
struct ServerConfig {
    #[serde(rename = "bind-address")]
    bind_address: String,
}

映射关系:

bind-address → bind_address

5. #[serde(rename_all = "lowercase")]

适合枚举。

#[derive(Debug, Deserialize)]
#[serde(rename_all = "lowercase")]
enum Environment {
    Development,
    Test,
    Production,
}

TOML:

environment = "production"

解析结果:

Environment::Production

如果不加:

#[serde(rename_all = "lowercase")]

那么默认可能要求配置写成:

environment = "Production"

6. #[serde(deny_unknown_fields)]

用于禁止配置文件出现未知字段。

#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct ServerConfig {
    host: String,
    port: u16,
}

假设配置写错:

[server]
host = "0.0.0.0"
prot = 5800

这里把 port 错写成了 prot

启用 deny_unknown_fields 后,会直接报错,而不是悄悄忽略。正式项目推荐在配置结构体上使用它,因为配置字段拼写错误通常应该阻止程序启动。TOML 反序列化错误类型能够表示未知字段、缺少字段、重复字段和错误类型等问题。(Docs.rs)

八、解析数组

TOML:

allowed_origins = [
    "http://localhost:5173",
    "https://admin.example.com",
]

Rust:

use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct CorsConfig {
    allowed_origins: Vec<String>,
}

读取:

for origin in &config.allowed_origins {
    println!("{origin}");
}

混合类型数组

TOML 1.1 允许数组中出现不同类型:

values = [1, "hello", true]

规范允许这种混合数组。(toml.io)

但下面的 Rust 类型无法接收它:

values: Vec<String>

因为数组元素并不全是字符串。

动态配置可以使用:

values: Vec<toml::Value>

例如:

#[derive(Debug, Deserialize)]
struct Config {
    values: Vec<toml::Value>,
}

处理:

for value in &config.values {
    match value {
        toml::Value::String(value) => {
            println!("字符串:{value}");
        }

        toml::Value::Integer(value) => {
            println!("整数:{value}");
        }

        toml::Value::Boolean(value) => {
            println!("布尔值:{value}");
        }

        _ => {
            println!("其他类型");
        }
    }
}

不过,业务配置最好保持数组类型一致。

九、使用 HashMap 读取动态表名

假设不同服务的名称是不固定的。

TOML

[services.user]
base_url = "http://127.0.0.1:8101"
timeout_seconds = 5

[services.ticket]
base_url = "http://127.0.0.1:8102"
timeout_seconds = 10

[services.message]
base_url = "http://127.0.0.1:8103"
timeout_seconds = 8

Rust

use serde::Deserialize;
use std::collections::HashMap;

#[derive(Debug, Deserialize)]
struct AppConfig {
    services: HashMap<String, ServiceConfig>,
}

#[derive(Debug, Deserialize)]
struct ServiceConfig {
    base_url: String,
    timeout_seconds: u64,
}

使用:

for (name, service) in &config.services {
    println!(
        "服务名称:{},地址:{},超时:{} 秒",
        name,
        service.base_url,
        service.timeout_seconds
    );
}

访问指定服务:

if let Some(ticket_service) =
    config.services.get("ticket")
{
    println!(
        "工单服务地址:{}",
        ticket_service.base_url
    );
}

需要注意,TOML 的表键是字符串,所以动态表通常应该映射为:

HashMap<String, ServiceConfig>

而不是:

HashMap<u64, ServiceConfig>

toml::Table::try_from() 在遇到非字符串 Map 键时可能转换失败。(Docs.rs)

十、把 Rust 结构体序列化成 TOML

需要为结构体派生:

Serialize

示例:

use serde::{Deserialize, Serialize};
use std::error::Error;

#[derive(Debug, Serialize, Deserialize)]
struct AppConfig {
    name: String,
    server: ServerConfig,
}

#[derive(Debug, Serialize, Deserialize)]
struct ServerConfig {
    host: String,
    port: u16,
}

fn main() -> Result<(), Box<dyn Error>> {
    let config = AppConfig {
        name: "IT工单系统".to_string(),

        server: ServerConfig {
            host: "0.0.0.0".to_string(),
            port: 5800,
        },
    };

    let content = toml::to_string(&config)?;

    println!("{content}");

    Ok(())
}

to_string()to_string_pretty()

普通序列化:

let content = toml::to_string(&config)?;

格式化序列化:

let content = toml::to_string_pretty(&config)?;

to_string_pretty() 会生成更适合人类阅读的 TOML 文本。(Docs.rs)

实际项目建议使用:

toml::to_string_pretty()

写入文件

use serde::Serialize;
use std::error::Error;
use std::fs;

#[derive(Debug, Serialize)]
struct ServerConfig {
    host: String,
    port: u16,
}

fn main() -> Result<(), Box<dyn Error>> {
    let config = ServerConfig {
        host: "0.0.0.0".to_string(),
        port: 5800,
    };

    let content = toml::to_string_pretty(&config)?;

    fs::write(
        "generated.toml",
        content,
    )?;

    println!("配置文件生成成功");

    Ok(())
}

十一、修改已有配置后重新写入

use serde::{Deserialize, Serialize};
use std::error::Error;
use std::fs;

#[derive(Debug, Serialize, Deserialize)]
struct AppConfig {
    server: ServerConfig,
}

#[derive(Debug, Serialize, Deserialize)]
struct ServerConfig {
    host: String,
    port: u16,
}

fn main() -> Result<(), Box<dyn Error>> {
    let content =
        fs::read_to_string("application.toml")?;

    let mut config: AppConfig =
        toml::from_str(&content)?;

    config.server.port = 8080;

    let new_content =
        toml::to_string_pretty(&config)?;

    fs::write(
        "application.toml",
        new_content,
    )?;

    Ok(())
}

流程为:

读取文件
    ↓
解析成结构体
    ↓
修改结构体
    ↓
序列化
    ↓
覆盖文件

十二、环境变量覆盖 TOML 配置

需要注意,toml 库不会自动处理这种占位符:

url = "${DATABASE_URL}"

它只会把 ${DATABASE_URL} 当成普通字符串。

可以先读取 TOML,再使用环境变量覆盖:

use std::env;

let mut config =
    load_config("config/application.toml")?;

if let Ok(database_url) =
    env::var("DATABASE_URL")
{
    config.database.url = database_url;
}

if let Ok(redis_url) =
    env::var("REDIS_URL")
{
    if let Some(redis) = &mut config.redis {
        redis.url = redis_url;
    }
}

推荐的优先级是:

代码默认值
    ↓
application.toml
    ↓
环境变量

例如普通配置放在 TOML:

[database]
min_connections = 5
max_connections = 20

敏感内容通过环境变量提供:

DATABASE_URL
REDIS_URL
JWT_SECRET

不要把生产数据库密码、JWT 密钥、云平台 Secret 等敏感数据提交到 Git 仓库中的 TOML 文件。

十三、TOML 日期时间

TOML 原生支持日期和时间。

配置文件

[release]
released_at = 2026-07-19T20:30:00+08:00
maintenance_date = 2026-07-20

Rust

use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct Config {
    release: ReleaseConfig,
}

#[derive(Debug, Deserialize)]
struct ReleaseConfig {
    released_at: toml::value::Datetime,
    maintenance_date: toml::value::Datetime,
}

使用:

println!(
    "发布时间:{}",
    config.release.released_at
);

toml::Value 中专门提供了 Datetime 类型,用于表示 TOML 的日期时间。(Docs.rs)

十四、解析错误处理

假设 TOML 写错:

[server]
host = "0.0.0.0"
port = "5800"

而 Rust 要求:

port: u16

解析代码:

match toml::from_str::<AppConfig>(&content) {
    Ok(config) => {
        println!("{config:#?}");
    }

    Err(error) => {
        eprintln!("配置解析失败:{error}");

        eprintln!("错误原因:{}", error.message());

        if let Some(span) = error.span() {
            eprintln!(
                "错误字节范围:{}..{}",
                span.start,
                span.end
            );
        }
    }
}

toml::de::Error 提供:

error.message()
error.span()

其中 span() 返回错误在原始 TOML 文档中的字节范围;错误的 Display 输出通常还会包含行号、列号和具体解析上下文。(Docs.rs)

十五、常见错误

1. 必填字段缺失

Rust:

struct ServerConfig {
    host: String,
    port: u16,
}

TOML:

[server]
host = "0.0.0.0"

因为缺少 port,解析失败。

解决方式之一是补配置:

port = 5800

也可以提供默认值:

#[serde(default = "default_port")]
port: u16

或者改为可选:

port: Option<u16>

2. 类型不匹配

错误:

port = "5800"

正确:

port = 5800

因为:

"5800" → String
5800   → Integer

3. 布尔值大小写错误

错误:

enabled = True
enabled = FALSE

正确:

enabled = true
enabled = false

TOML 布尔值只能使用小写的 truefalse。(toml.io)

4. 重复定义键

错误:

port = 5800
port = 8080

同一个键不能重复定义。(toml.io)

5. 重复定义表

错误:

[server]
host = "0.0.0.0"

[server]
port = 5800

同一个普通表不能定义两次。应该合并为:

[server]
host = "0.0.0.0"
port = 5800

6. 配置文件路径错误

下面路径是相对于程序的“当前工作目录”,不一定是相对于 main.rs

fs::read_to_string("config/application.toml")

执行:

cargo run

时,通常当前目录是项目根目录。

但部署为系统服务、Docker 容器或 Windows 服务后,当前目录可能发生变化。正式项目可以通过启动参数或环境变量明确提供配置文件路径:

APP_CONFIG_PATH=/data/workorder/application.toml

十六、键顺序和注释问题

toml::Table 默认按照键的字典顺序存储;启用 preserve_order feature 后,可以保留输入中的项目顺序。(Docs.rs)

配置:

toml = {
    version = "1",
    features = ["preserve_order"],
}

但需要特别注意:

toml::from_str()
    ↓
Rust 结构体或 toml::Table
    ↓
toml::to_string_pretty()

这个过程主要关注“数据”,不适合要求完整保留原配置文件的:

  • 注释;
  • 空行;
  • 原始缩进;
  • 原始引号风格;
  • 所有排版细节。

例如原文件:

# 服务端配置
[server]

# HTTP 端口
port = 5800

解析后再序列化,注释通常不会保留下来。

需要修改 TOML,同时保留注释、空格和项目相对顺序时,应使用同一项目提供的 toml_edit 库。toml_edit 的定位就是格式保留型 TOML 编辑。(Docs.rs)

简单区分:

读取应用配置、映射结构体
    → toml

修改 Cargo.toml,并保留注释和格式
    → toml_edit

十七、toml::Table 和结构体该怎么选

使用结构体

适用于:

application.toml
database.toml
server.toml
系统启动配置
数据库配置
Redis 配置
JWT 配置
业务功能开关

推荐写法:

let config: AppConfig =
    toml::from_str(&content)?;

这是正式项目的首选。

使用 toml::Table

适用于:

配置结构不确定
插件自定义配置
动态键值配置
临时读取字段
配置检查工具
通用 TOML 查看器

推荐写法:

let table: toml::Table =
    content.parse()?;

使用 toml::Value

适用于:

字段类型不确定
数组允许多种类型
需要递归遍历全部 TOML 内容
开发通用解析工具

十八、推荐的项目目录

结合 Rust Web 项目,可以这样组织:

workorder-server/
├── Cargo.toml
├── config/
│   ├── application.toml
│   ├── application-dev.toml
│   ├── application-test.toml
│   └── application-prod.toml
└── src/
    ├── main.rs
    ├── lib.rs
    └── config/
        ├── mod.rs
        ├── model.rs
        ├── error.rs
        └── loader.rs

不使用 mod.rs 的 Rust 2024 风格也可以写成:

src/
├── main.rs
├── config.rs
└── config/
    ├── model.rs
    ├── error.rs
    └── loader.rs

src/config.rs

pub mod error;
pub mod loader;
pub mod model;

pub use error::ConfigError;
pub use loader::load_config;
pub use model::AppConfig;

十九、核心 API 总结

解析成结构体:

let config: AppConfig =
    toml::from_str(&content)?;

解析成动态表:

let table: toml::Table =
    content.parse()?;

序列化:

let content =
    toml::to_string(&config)?;

格式化序列化:

let content =
    toml::to_string_pretty(&config)?;

读取文件:

let content =
    std::fs::read_to_string(path)?;

写入文件:

std::fs::write(path, content)?;

读取动态字段:

table
    .get("server")
    .and_then(toml::Value::as_table)

读取字符串:

value.as_str()

读取整数:

value.as_integer()

读取布尔值:

value.as_bool()

修改表:

value.as_table_mut()

二十、实际项目推荐方案

对于你的 Rust 2024、Salvo、SeaORM、MySQL 8、Redis 6 项目,推荐采用:

TOML
    ↓
std::fs::read_to_string
    ↓
toml::from_str
    ↓
强类型 AppConfig
    ↓
环境变量覆盖敏感配置
    ↓
启动前校验配置
    ↓
将 AppConfig 注入 Salvo 应用状态

关键原则是:

固定配置结构 → Rust 结构体

可选配置 → Option<T>

有默认值配置 → #[serde(default)]

防止拼写错误 → #[serde(deny_unknown_fields)]

动态服务名称 → HashMap<String, T>

多个同类配置 → Vec<T> + [[table]]

敏感配置 → 环境变量

保留注释修改 → toml_edit

其中最重要的一行代码就是:

let config: AppConfig =
    toml::from_str(&content)?;

它负责把整个 TOML 配置文件安全地转换成强类型 Rust 配置对象。