不灭的焱

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

作者:AlbertWen  添加时间:2026-07-19 21:46:38  修改时间:2026-07-26 02:05:00  分类:01.Rust编程  编辑

目录

AI提问:

在 Rust 多应用、多模块项目中,各个应用和模块通常会在各自的 Cargo.toml 中声明依赖,因此容易出现同一个依赖重复配置、版本不一致的问题。

如何统一管理这些公共依赖的版本,确保各个应用和模块使用相同版本?是否可以像设置全局变量一样集中声明依赖版本,以便后续升级时只修改一处即可?

AI回复:

可以。Rust 的 Cargo Workspace 原生提供了“统一管理依赖版本”的功能:

[workspace.dependencies]

它相当于多模块项目里的全局依赖版本配置。以后升级依赖时,只需要修改根目录的 Cargo.toml,各应用和模块都会继承新的版本。Cargo 官方称其为 Workspace dependency inheritance(工作空间依赖继承)。(Rust 文档)

一、推荐的项目结构

例如一个包含多个后端应用和公共模块的 Rust 项目:

my-project/
├── Cargo.toml
├── Cargo.lock
│
├── apps/
│   ├── api-server/
│   │   ├── Cargo.toml
│   │   └── src/
│   └── task-worker/
│       ├── Cargo.toml
│       └── src/
│
└── crates/
    ├── common/
    │   ├── Cargo.toml
    │   └── src/
    ├── config/
    │   ├── Cargo.toml
    │   └── src/
    └── database/
        ├── Cargo.toml
        └── src/

其中:

  • apps/api-server:API 服务
  • apps/task-worker:后台任务服务
  • crates/common:公共工具
  • crates/config:配置管理
  • crates/database:数据库访问

所有成员共享根目录的 Cargo.locktarget 目录。(Rust 文档)

二、在根目录统一声明依赖

根目录的 Cargo.toml

[workspace]
resolver = "3"
members = [
    "apps/api-server",
    "apps/task-worker",
    "crates/common",
    "crates/config",
    "crates/database",
]

[workspace.package]
version = "0.1.0"
edition = "2024"
rust-version = "1.85"

[workspace.dependencies]
# 异步运行时
tokio = { version = "1.48.0", features = ["full"] }

# Web 框架
salvo = { version = "0.85.0", features = ["affix-state"] }

# 序列化
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145"

# 配置
toml = "0.9.8"
dotenvy = "0.15.7"

# 错误处理
anyhow = "1.0.100"
thiserror = "2.0.17"

# 日志
tracing = "0.1.41"
tracing-subscriber = { version = "0.3.20", features = ["env-filter"] }

# 数据库
sea-orm = {
    version = "1.1.17",
    features = [
        "sqlx-mysql",
        "runtime-tokio-rustls",
        "macros",
    ],
}

# 工作空间内部模块
common = { path = "crates/common" }
config = { path = "crates/config" }
database = { path = "crates/database" }

这里的:

[workspace.dependencies]

就是你要找的“全局依赖版本配置”。

但它不属于传统编程语言里的变量,Cargo 也不支持下面这种自定义变量插值:

# 不支持这种写法
[variables]
tokio_version = "1.48.0"

[dependencies]
tokio = "${tokio_version}"

应该直接使用 Cargo 提供的 [workspace.dependencies]

三、子应用继承根目录依赖

1. api-server

apps/api-server/Cargo.toml

[package]
name = "api-server"
version.workspace = true
edition.workspace = true
rust-version.workspace = true

[dependencies]
tokio.workspace = true
salvo.workspace = true
serde.workspace = true
serde_json.workspace = true
anyhow.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true

common.workspace = true
config.workspace = true
database.workspace = true

下面两种写法完全等价:

tokio.workspace = true

等价于:

tokio = { workspace = true }

通常推荐第一种,比较简洁。

2. task-worker

apps/task-worker/Cargo.toml

[package]
name = "task-worker"
version.workspace = true
edition.workspace = true
rust-version.workspace = true

[dependencies]
tokio.workspace = true
serde.workspace = true
serde_json.workspace = true
anyhow.workspace = true
tracing.workspace = true

common.workspace = true
config.workspace = true
database.workspace = true

这里没有再次写:

tokio = "1.48.0"

而是:

tokio.workspace = true

因此,api-servertask-worker 使用的 Tokio 版本要求都来自根目录。

3. database 模块

crates/database/Cargo.toml

[package]
name = "database"
version.workspace = true
edition.workspace = true
rust-version.workspace = true

[dependencies]
sea-orm.workspace = true
serde.workspace = true
thiserror.workspace = true

config.workspace = true

四、以后如何统一升级版本

假设现在根目录是:

[workspace.dependencies]
tokio = { version = "1.48.0", features = ["full"] }

以后要升级 Tokio,只修改根目录:

[workspace.dependencies]
tokio = { version = "1.49.0", features = ["full"] }

然后执行:

cargo update -p tokio

或者重新构建:

cargo build --workspace

所有使用:

tokio.workspace = true

的应用和模块都会使用新的统一版本要求。

五、Cargo.tomlCargo.lock 的区别

这个区别非常重要。

Cargo.toml:声明版本范围

tokio = "1.48.0"

它通常并不表示“只能使用 1.48.0”。

Cargo 默认使用 ^ 兼容规则,上面的写法大致相当于:

tokio = "^1.48.0"

允许的范围是:

>= 1.48.0
< 2.0.0

因此,Cargo 可能最终解析到:

1.49.0
1.50.0
……

只要没有进入 2.x

Cargo.lock:记录实际安装版本

例如:

[[package]]
name = "tokio"
version = "1.49.0"

Workspace 内的所有包共享根目录的一个 Cargo.lock,后续构建会尽量继续使用锁文件中记录的实际版本。(Rust 文档)

可以这样理解:

文件 作用
Cargo.toml 声明允许使用什么版本
Cargo.lock 记录本次实际使用什么版本

六、怎样严格锁定一个版本

如果你要求必须使用指定版本,例如必须是:

tokio 1.48.0

可以加等号:

[workspace.dependencies]
tokio = { version = "=1.48.0", features = ["full"] }

含义是:

只能使用 1.48.0

以下写法的区别:

tokio = "1.48.0"

表示:

>= 1.48.0 且 < 2.0.0

而:

tokio = "=1.48.0"

表示:

只能是 1.48.0

不过通常不建议对所有库都使用 =,因为限制过严可能导致依赖解析冲突。Cargo 官方也建议仅在确实需要严格同步、已知新版本有兼容问题等场景使用精确版本。(Rust 文档)

对于普通业务项目,我更推荐:

tokio = "1.48.0"

然后:

  • 提交根目录的 Cargo.lock
  • CI 使用 cargo build --locked
  • 依赖升级时主动执行 cargo update

这样既统一,又不至于把版本条件限制得过死。

七、某个应用需要额外 Feature 怎么办

假设根目录定义:

[workspace.dependencies]
serde = { version = "1.0.228", features = ["derive"] }

普通模块:

[dependencies]
serde.workspace = true

某个应用还需要 rc 功能:

[dependencies]
serde = {
    workspace = true,
    features = ["rc"],
}

最终该应用获得的功能是:

derive + rc

子模块增加的 features 会和根目录声明的 features 合并,而不是覆盖。(Rust 文档)

八、default-features 最好在哪里配置

例如所有项目都不需要 Tokio 的默认功能,可以在根目录统一设置:

[workspace.dependencies]
tokio = {
    version = "1.48.0",
    default-features = false,
    features = [
        "rt-multi-thread",
        "macros",
        "signal",
    ],
}

成员中只需要:

[dependencies]
tokio.workspace = true

不要在子模块里试图修改:

tokio = {
    workspace = true,
    default-features = false,
}

继承依赖时,成员主要可以额外指定:

  • features
  • optional

其他依赖属性,例如 versiondefault-features,应该在根 Workspace 中统一定义。(Rust 文档)

九、可选依赖如何处理

根 Workspace 不能直接把依赖声明成 optional = true

# 不推荐,在 workspace.dependencies 中不能这样声明
[workspace.dependencies]
redis = {
    version = "0.32.0",
    optional = true,
}

应该在根目录只声明版本:

[workspace.dependencies]
redis = "0.32.0"

然后在需要可选功能的成员中声明:

[dependencies]
redis = {
    workspace = true,
    optional = true,
}

[features]
default = []
redis-cache = ["dep:redis"]

这样:

cargo build

不会启用 Redis。

执行:

cargo build --features redis-cache

才会启用 Redis。

十、开发依赖和构建依赖也能继承

不仅普通依赖可以统一管理,下面这些都支持 Workspace 继承:

[dependencies]
[dev-dependencies]
[build-dependencies]
[target.'cfg(...)'.dependencies]

例如根目录:

[workspace.dependencies]
tokio = { version = "1.48.0", features = ["full"] }
mockall = "0.13.1"
cc = "1.2.49"

成员模块:

[dependencies]
tokio.workspace = true

[dev-dependencies]
mockall.workspace = true

[build-dependencies]
cc.workspace = true

这些都是 Cargo 官方支持的继承方式。(Rust 文档)

十一、内部模块也应该统一声明

多模块项目中,内部模块可以一起放进 [workspace.dependencies]

根目录:

[workspace.dependencies]
common = { path = "crates/common" }
config = { path = "crates/config" }
database = { path = "crates/database" }

应用中:

[dependencies]
common.workspace = true
config.workspace = true
database.workspace = true

相比每个模块重复写:

common = { path = "../../crates/common" }
config = { path = "../../crates/config" }
database = { path = "../../crates/database" }

这种方式更加统一,也避免相对路径层级错误。

十二、依赖不会自动加到所有模块

根目录定义了:

[workspace.dependencies]
tokio = "1.48.0"
serde = "1.0.228"
sea-orm = "1.1.17"

并不代表所有成员都会自动依赖它们。

每个成员仍然要显式选择:

[dependencies]
tokio.workspace = true
serde.workspace = true

如果某个模块不需要 SeaORM,就不要声明:

sea-orm.workspace = true

这种设计可以避免每个子模块都携带大量无关依赖。

十三、如何检查是否存在重复版本

可以执行:

cargo tree --duplicates

简写:

cargo tree -d

例如输出:

http v0.2.12
└── old-library v1.0.0

http v1.3.1
└── salvo v0.85.0

这说明依赖树里同时存在两个不兼容版本:

http 0.2.12
http 1.3.1

查看某个库被谁依赖:

cargo tree -i tokio

查看指定版本:

cargo tree -i tokio@1.48.0

需要注意:即使你统一了所有直接依赖,也可能因为第三方库的间接依赖要求不同,出现同一个库的多个大版本。例如:

库 A 依赖 http 0.2
库 B 依赖 http 1.x

这种情况下,Cargo 可能必须同时编译两个版本。这并不是 [workspace.dependencies] 能完全消除的,因为它主要统一的是你项目中的直接依赖声明

十四、推荐的完整配置

对于你的 Rust 多应用、多模块项目,可以采用下面这套写法。

根目录 Cargo.toml

[workspace]
resolver = "3"
members = [
    "apps/*",
    "crates/*",
]

[workspace.package]
version = "0.1.0"
edition = "2024"
rust-version = "1.85"
license = "MIT"
authors = ["Your Team"]

[workspace.dependencies]
# Web
salvo = "0.85.0"

# Async
tokio = {
    version = "1.48.0",
    features = ["full"],
}

# Serialization
serde = {
    version = "1.0.228",
    features = ["derive"],
}
serde_json = "1.0.145"
toml = "0.9.8"

# Database
sea-orm = {
    version = "1.1.17",
    features = [
        "sqlx-mysql",
        "runtime-tokio-rustls",
        "macros",
    ],
}

# Configuration
dotenvy = "0.15.7"

# Error handling
anyhow = "1.0.100"
thiserror = "2.0.17"

# Logging
tracing = "0.1.41"
tracing-subscriber = {
    version = "0.3.20",
    features = ["env-filter"],
}

# Internal crates
common = { path = "crates/common" }
config = { path = "crates/config" }
database = { path = "crates/database" }

成员模块:

[package]
name = "api-server"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
authors.workspace = true

[dependencies]
salvo.workspace = true
tokio.workspace = true
serde.workspace = true
serde_json.workspace = true
anyhow.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true

common.workspace = true
config.workspace = true
database.workspace = true

十五、最终建议

你的场景建议采用:

[workspace.dependencies]

核心规则是:

# 根 Cargo.toml
[workspace.dependencies]
tokio = "1.48.0"
serde = "1.0.228"
# 子模块 Cargo.toml
[dependencies]
tokio.workspace = true
serde.workspace = true

版本升级时只改根目录:

tokio = "1.49.0"

然后执行:

cargo update -p tokio
cargo check --workspace
cargo test --workspace

生产和 CI 构建建议使用:

cargo build --release --locked

这样可以保证构建严格使用已经提交的 Cargo.lock,避免 CI 或生产构建时悄悄解析出不同版本。