23-Cargo 工作流

面向 Go 用户讲清 Cargo 的日常命令、锁文件、feature、workspace 与发布流程

Cargo 工作流 (Cargo Workflow)

面向 Rust 1.97.1 (stable, 2026-07)。如果你把 Cargo 只理解成“下载依赖的工具”,这一篇会帮你把它扩展成完整的日常工作流心智模型。

你会看到的不是一堆零散命令,而是“建项目、改依赖、加检查、发版本、做 CI”这些真实动作在 Cargo 里分别落到哪里。

Cargo 同时承担了包管理、构建、测试、文档、发布和 workspace 调度。对 Go 用户来说,差异最大的是:Cargo 把更多工程化决策显式做成了配置和命令入口。

Q1. cargo new 和 cargo init 有什么区别?

Tags: hot cargo new cargo init 适用版本: Cargo

一句话答案: cargo new 用来创建新目录并初始化项目,cargo init 用来把已有目录变成 Cargo 项目。

解答: 前者适合从零开项目,后者适合你已经有代码仓库、文档目录或空文件夹,只差把 Rust 工程骨架补进去。它们都会生成 Cargo.toml 和 src/ 结构,只是对现有目录的处理不同。

1
2
3
4
cargo new hello-rust
cargo new --lib math-core
cargo init
cargo init --bin .

cargo new 默认创建新目录;cargo init 会在当前目录或指定目录上操作,因此更适合把现有仓库接入 Cargo。

Go 对比:

  • Go 通常是新建目录后自己 go mod init,再手工放源码。
  • Cargo 把目录骨架和 manifest 一起生成,起步更完整。
  • 已有仓库要接入 Rust 时,cargo init 的定位最像“补一个工程骨架”。

记忆点:

  • 新目录:cargo new。
  • 已有目录:cargo init。
  • --lib 和 --bin 决定初始产物类型。

Q2. 日常最常用的 cargo build、run、check、test 怎么分工?

Tags: hot build check test 适用版本: Cargo

一句话答案: build 产出可执行文件,run 构建后运行,check 只做类型检查不产出最终二进制,test 则构建并执行测试目标。

解答: 对迭代开发来说,cargo check 往往是最快的反馈回路;只有当你需要真正运行程序、链接产物或验证基准行为时,再切到 cargo run / cargo build。

1
2
3
4
cargo check
cargo build
cargo run -- arg1 arg2
cargo test

cargo check 不会跳过借用检查和类型检查,它只是省去了后面的代码生成与链接阶段,因此经常比完整构建快很多。

Go 对比:

  • Go 的 go test ./... 和 go build 也承担检查与构建,但 cargo check 这个“只做到编译前半程”的命令更常用。
  • Rust 编译成本更高,因此把 check 当日常主循环非常划算。
  • 改完代码先 check,准备交付前再 test/build,是很常见的节奏。

记忆点:

  • 写代码时先 cargo check。
  • 要运行就 cargo run。
  • 要产出制品或验证发布模式,就 cargo build --release。

Q3. Cargo.toml 和 Cargo.lock 各自记录什么?

Tags: hot Cargo.toml Cargo.lock 适用版本: Cargo

一句话答案: Cargo.toml 记录“你希望依赖什么”,Cargo.lock 记录“这次实际解析到了哪些精确版本”;二进制默认提交 lock,纯库通常不提交。

解答: Cargo.toml 更像需求声明,允许写版本范围、feature、profile 等;Cargo.lock 则是解析结果快照,用来让同一项目在不同机器和不同时间尽量复现同一依赖图。

1
2
3
[dependencies]
serde = "1"
clap = { version = "4", features = ["derive"] }

真实的 Cargo.lock 会带上精确版本、来源、checksum,以及该包解析出的 dependencies 列表,例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4

[[package]]
name = "serde"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
 "serde_core",
]

Cargo.toml 里写 serde = "1" 只是范围;Cargo.lock 才钉死“这次编出来用的是哪一个精确版本、校验和是什么、还拉进了哪些传递依赖”。

默认提交建议:

1
2
3
4
5
# 二进制 / 应用 / CLI:默认提交,保证 CI 与部署可复现
git add Cargo.toml Cargo.lock

# 准备 publish 到 crates.io 的纯库:通常不提交 Cargo.lock
# (下游会按自己的依赖图重新解析;细节见 [Q4](#q4))

如果你只删 Cargo.lock 想“碰碰运气修问题”,很可能只是偷偷换了一套依赖组合,而不是解决真正根因。

Go 对比:

  • go.mod 兼有需求声明和一部分最小版本信息,go.sum 更偏校验摘要。
  • Cargo 把“声明”和“最终解析结果”分得更开。
  • 这也是为什么 Rust 项目里 Cargo.lock 的存在感通常比 go.sum 更强。

记忆点:

  • Cargo.toml 是意图,Cargo.lock 是带 checksum 的解析结果。
  • 二进制默认提交 lock;纯库通常不提交。
  • 删 lock 文件不是常规修复手段。

Q4. Cargo.lock 到底要不要提交到仓库?

Tags: common Cargo.lock 适用版本: Cargo

一句话答案: 应用和二进制项目通常应该提交;发布到 crates.io 的纯库默认通常不提交;带示例/集成测试/workspace 的库仓库则建议提交。

解答: 对应用来说,提交 Cargo.lock 能显著提升构建可复现性,CI 和线上部署都更稳。对公开库来说,消费者最终仍会按自己的依赖图重新解析,因此发布到 crates.io 的纯库默认通常不提交 lock;但如果这个库仓库本身也带示例、工具、集成测试,或处在 workspace 里,提交 lock 反而更省心。

1
2
3
4
5
# 应用 / 二进制:默认提交
git add Cargo.toml Cargo.lock

# 纯库(准备 publish 到 crates.io):默认可不提交 Cargo.lock
# 但若仓库内有 examples/、tests/ 或 workspace:建议提交

默认可执行建议可以记成三条:

  1. 应用 / CLI / 服务:默认提交 Cargo.lock。
  2. 发布到 crates.io 的纯库:默认不提交 Cargo.lock。
  3. 库仓里已有示例、集成测试或 workspace:建议提交,并在团队里固定这一条。

更重要的是团队约定一致:别今天提交、明天又在 review 里把它删掉。对应用项目,默认提交通常是最省心的选择。

Go 对比:

  • Go 没有一个和 Cargo.lock 作用完全一致的文件。
  • Rust 团队协作里,lock 文件更像正式制品的一部分,而不是临时缓存。
  • 如果你做的是最终部署的软件,提交 lock 文件几乎总是更稳的选择。

记忆点:

  • 应用项目默认提交 Cargo.lock。
  • 纯库默认通常不提交;带示例/workspace 则建议提交。
  • 把 lock 文件当缓存删掉,往往会制造新变量。

Q5. cargo add、cargo remove、cargo update 各做什么?

Tags: common dependencies 适用版本: cargo add/remove 内置于 Cargo 1.62+

一句话答案: add 改 manifest、remove 删依赖、update 刷新锁文件里的已解析版本。

解答: 这三者处理的是依赖生命周期的不同阶段。很多人会把 cargo update 误解成“升级工具链”,其实它只是重新解析依赖图并更新 Cargo.lock。

1
2
3
4
5
cargo add serde --features derive
cargo add --dev insta
cargo remove insta
cargo update
cargo update -p serde

当你只想升级某个包并观察影响时,cargo update -p name 比直接删 lock 文件可控得多。

Go 对比:

  • Go 里你常用 go get 既改模块需求又触发版本调整。
  • Cargo 把这些动作拆得更细,因此意图更明确。
  • 工具链升级不是 cargo update,那是 rustup update 的事。

记忆点:

  • 改 manifest:add / remove。
  • 刷锁文件:update。
  • 局部升级优先 cargo update -p。

Q6. cargo install 为什么不是“给当前项目装依赖”?

Tags: common cargo install 适用版本: Cargo

一句话答案: 因为 cargo install 安装的是可执行 crate 到你的用户环境,不会修改当前项目的 Cargo.toml。

解答: 这和 cargo add 经常被初学者混淆。cargo install 适合装开发工具,比如 cargo-watch、cargo-nextest、just 之类;如果你想让当前项目能 use serde::...,应该用 cargo add serde。

1
2
3
cargo add serde
cargo install cargo-watch
cargo install --path .

最后一条 cargo install --path . 的意思是“把当前 package 产出的二进制安装到用户环境里”,常用于本地体验 CLI。

Go 对比:

  • Go 的 go install 也偏向“安装一个可执行工具”,这一点和 Cargo 很像。
  • 但 Go 用户刚转 Rust 时容易误以为 install 会顺手改项目依赖,这点要特别分清。
  • 想改项目依赖,一律先想到 cargo add。

记忆点:

  • 项目依赖用 cargo add。
  • 全局工具用 cargo install。
  • install 不会改当前项目 manifest。

Q7. profile.dev 和 profile.release 该怎么理解?

Tags: common profile 适用版本: Cargo

一句话答案: profile 是构建配置模板,用来决定优化级别、调试信息、panic 策略等,而不是另一套独立工程配置。

解答: 开发阶段通常更关心编译速度和调试友好性,发布阶段则更看重体积、性能和行为一致性。Cargo 用 profile 把这些开关集中起来管理。

1
2
3
4
5
6
[profile.dev]
opt-level = 0

[profile.release]
opt-level = 3
panic = "abort"

最常见的实践是:默认 dev 跑测试和调试,需要测真实性能时再显式 cargo build --release 或 cargo run --release。

Go 对比:

  • Go 的构建模式旋钮相对更少,很多优化决策不需要你手动显式配置。
  • Cargo profile 给了更多控制面,因此也要求你更清楚自己是在“开发反馈回路”还是“发布制品回路”。
  • 性能结论尽量基于 release 构建,不要在 dev 模式下下判断。

记忆点:

  • profile 是构建参数集合。
  • 调试看 dev,性能看 release。
  • 测性能时别忘了 --release。

Q8. feature 在命令行上怎么开、怎么关、怎么排查?

Tags: common features 适用版本: Cargo

一句话答案: 用 --features、--no-default-features、--all-features 控制;用 cargo tree -e features 看依赖图里 feature 是怎么被打开的。

解答: feature 问题常见于“为什么某个可选依赖突然被编进来了”或者“为什么我明明没开这个开关却表现像开了”。Cargo 的 feature 统一规则意味着你必须从整个依赖图角度排查,而不是只看当前 crate。

1
2
3
4
cargo build --features serde
cargo build --no-default-features
cargo test --all-features
cargo tree -e features

如果一个 feature 被多个依赖间接开启,cargo tree -e features 往往比肉眼翻 Cargo.toml 快得多。

Go 对比:

  • Go 没有 feature unification 这套常规工作流入口。
  • Rust 项目里“先看依赖图,再猜行为”是很重要的排查习惯。
  • 一旦 feature 设计成互斥,会在团队协作里不断制造麻烦。

记忆点:

  • 开 feature:--features。
  • 关默认 feature:--no-default-features。
  • 排查来源:cargo tree -e features。

Q9. workspace 日常最该记住哪些 Cargo 命令?

Tags: common workspace 适用版本: Cargo workspace

一句话答案: 先记住 --workspace、-p <package>、--exclude,它们分别对应“全仓跑”“只跑某个成员”“除了某些成员都跑”。

解答: workspace 一旦成员变多,最容易出现两种低效:要么每次全仓慢跑,要么忘了当前命令到底作用在哪个成员上。掌握这几个作用域开关后,命令意图会清楚很多。

1
2
3
cargo test --workspace
cargo check -p cli
cargo build --workspace --exclude playground

当 CI 和本地命令行为不一致时,先比对有没有 --workspace 或 -p 这类作用域差异。

Go 对比:

  • Go 常见是 ./... 这种路径展开语义。
  • Cargo 更偏向 package/workspace 维度调度,因此命令式更强。
  • 大仓库里先选好作用域,再谈优化速度。

记忆点:

  • 全仓:--workspace。
  • 单成员:-p name。
  • 先确定作用域,再跑命令。

Q10. CI 里最常见的 Cargo 检查流水线长什么样?

Tags: common CI 适用版本: Cargo

一句话答案: 最小实用流水线通常是 fmt、clippy、test 三件套,再按项目需要补 doc、bench 或多目标构建。

解答: 这套顺序的目的不是凑热闹,而是让“格式问题”“明显 lint 问题”“行为问题”尽量在不同阶段快速失败。很多团队还会先跑一次 cargo check --workspace 做快反馈,再跑更完整的测试矩阵。

1
2
3
cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace

如果项目面向公开库,通常还会补一条 cargo doc --no-deps 看文档能否生成。

Go 对比:

  • Go 团队常见是 gofmt、go vet、go test ./...。
  • Rust 的对应物更分工明确,但整体思路完全类似:先静态检查,再行为验证。
  • CI 慢的时候,先区分是构建目标太多、测试太多,还是每次都在重复做全仓工作。

记忆点:

  • 常见最小流水线:fmt + clippy + test。
  • --workspace --all-targets 能覆盖得更全。
  • 先有稳定基线,再谈优化 CI 时长。

Q11. 内网或离线环境下,Cargo 依赖怎么稳定下来?

Tags: common vendor offline 适用版本: Cargo

一句话答案: 用 cargo vendor 把依赖拉到仓库内或制品库里,再通过 .cargo/config.toml 把 crates.io 重定向过去。

解答: 这在内网、可重复构建、长期归档场景里很常见。和只靠本机缓存不同,vendor 是显式把依赖来源纳入项目交付的一部分。

1
cargo vendor vendor
1
2
3
4
5
[source.crates-io]
replace-with = "vendored-sources"

[source.vendored-sources]
directory = "vendor"

做完后最好再在干净环境里跑一次构建,确认没有偷偷依赖用户主目录下的旧缓存。

Go 对比:

  • Go 也有 vendor/ 机制,但很多团队日常不常开。
  • Cargo 在企业内网里使用 vendor 的存在感通常更高,因为依赖解析和构建复现常被放进交付要求里。
  • 只说“我本机能编”不算离线方案,关键是别的机器能不能一样编。

记忆点:

  • cargo vendor 是正式方案,不是临时 hack。
  • 配套 .cargo/config.toml 才会真正走本地源。
  • 离线构建要在干净环境复验。

Q12. 发布 crate 前,最小检查清单应该有哪些?

Tags: common publish 适用版本: Cargo

一句话答案: 至少确认 metadata 补齐、cargo publish --dry-run 能过、无残留 path 依赖、README 和许可证正确,再决定是否正式发布。

解答: 很多发布事故不是代码错,而是 manifest 信息不全、包内容打错、私有文件泄漏或依赖源不合法。--dry-run 的价值就在于尽量把这些问题挡在真正上传之前。

1
2
3
cargo package
cargo publish --dry-run
cargo publish
1
2
3
4
5
6
[package]
name = "my-crate"
version = "0.1.0"
description = "A tiny example crate"
license = "MIT"
repository = "https://github.com/example/my-crate"

如果这是第一次对外发布,建议先看 cargo package --list 输出,确认最终被打包进去的文件真的是你想公开的那一批。

Go 对比:

  • Go 模块发布更多依赖代码托管平台的 tag 和路径约定。
  • Cargo 把发布动作集中到 crates.io 工作流里,因此 manifest 完整度更重要。
  • 发布前检查打包内容,在 Rust 里和检查 API 本身一样重要。

记忆点:

  • 正式发布前先 --dry-run。
  • manifest 元数据要完整。
  • path 依赖和错误打包内容是常见发布坑。

Q13. 依赖写成 ^1.2 / "1" 到底有多松?和 Cargo.lock 什么关系?

Tags: hot semver Cargo.lock 适用版本: Cargo

一句话答案: Cargo.toml 里的版本是兼容范围(有多松看写法),Cargo.lock 才钉死本次解析出的精确版本;有 lock 时构建可复现,改范围或 cargo update 才会换版本。

解答: Cargo 默认把 "1.2" 理解成 caret 需求 ^1.2,即允许 >=1.2.0, <2.0.0。写 "1" 等价 ^1,允许整个 1.x 最新兼容版本。看起来“只写了一个数字”,实际是一条范围,不是钉死补丁版。

1
2
3
4
[dependencies]
serde = "1"       # ^1  → 1.x 内可升级
clap  = "4.5"     # ^4.5 → >=4.5.0,<5.0.0
rand  = "=0.8.5"  # 精确钉死(少见,库更不推荐)
1
2
Cargo.toml  : 允许哪些版本参与解析
Cargo.lock  : 这次实际选中了哪几个精确版本 + checksum

应用提交了 Cargo.lock 时,别人 cargo build 会按 lock 复现,不会每次偷偷跳到范围内更新版本;想升级才跑 cargo update。纯库通常不提交 lock,下游会按自己的图在范围内再解析。

Go 对比:

  • 有点像 go.mod 里的版本需求 + 锁/求和校验的分工,但 Cargo 把“范围声明”和“解析快照”分得更开。
  • 看到 "1" 别当成“只用 1.0.0”。
  • 可复现构建看 lock,不看 toml 里的短版本号。

记忆点:

  • "1" / "1.2" 默认是 ^ 范围。
  • toml 管兼容窗口,lock 管精确复现。
  • 升级依赖靠 cargo update,不是改完 toml 就自动漂。

Q14. cargo tree 怎么查出是谁引进了某个 crate?

Tags: common cargo tree 适用版本: cargo tree 随 Cargo 提供(老版本可用 cargo install cargo-tree)

一句话答案: 用 cargo tree -i <crate> 反查“谁依赖了它”;再配合 -e/--edges 和 feature 相关选项看清引入路径。

解答: 直接依赖在 Cargo.toml 里一眼能看见;传递依赖才需要 cargo tree。最常用的是 invert:从目标 crate 往上翻到你的包。

1
2
3
4
cargo tree -i serde
cargo tree -i openssl --edges no-dev
cargo tree -i foo -p my_app
cargo tree --format "{p} {f}" | findstr serde

读输出时注意:同一 crate 可能因 semver 不兼容出现多个版本;-i 会分别展示。若只在测试里出现,确认是否来自 [dev-dependencies]。排查“为什么编进了不想要的依赖”时,先 -i 定位引入边,再决定是换依赖、关 feature,还是 [patch]/[exclude] 类手段。

Go 对比:

  • 类似 go mod why / go mod graph 的“谁拉进来的”问题。
  • Cargo 侧日常入口就是 cargo tree,尤其 -i。
  • 先查清引入路径,再改 toml,比盲目 cargo update 有效。

记忆点:

  • 反查用 cargo tree -i <name>。
  • 分清直接依赖和传递依赖。
  • 多版本并存时要对着 tree 看,不要猜。

Q15. build.rs 干什么用?和 go generate 像吗?

Tags: common build.rs 适用版本: Cargo

一句话答案: build.rs 是编译前由 Cargo 运行的构建脚本,用来探测系统、编译 C 库、codegen 或把结果告诉 rustc;和 go generate 部分像,但接入构建图更紧。

解答: 把 build.rs 放在 package 根目录后,Cargo 会在编译正式 crate 之前先编并执行它。脚本通过 println!("cargo:...") 指令与 Cargo 通信,例如设置 rustc-cfg、链接库、声明重跑条件。

1
2
3
4
5
6
7
8
// build.rs
fn main() {
    let target = std::env::var("TARGET").unwrap_or_default();
    println!("cargo:rerun-if-env-changed=TARGET");
    if target.contains("windows") {
        println!("cargo:rustc-cfg=platform_windows");
    }
}
1
2
3
# 构建脚本自己的依赖写这里,不会链进最终产物的普通依赖图同一位置
[build-dependencies]
cc = "1"

典型用途:用 cc 编本地 C 代码、查 pkg-config、根据环境生成绑定。它不是日常业务逻辑入口;能放进正常 Rust 源码的,就别塞进 build.rs。

Go 对比:

  • 像 go generate / cgo 前置步骤:都在“正式编译前”做事。
  • 不同点:build.rs 是 Cargo 一等公民,改输入会按 rerun-if-* 自动重跑。
  • Go 更常显式跑 generate;Rust 则常“有 build.rs 就会被 build 触发”。

记忆点:

  • build.rs = 编译前钩子。
  • 用 cargo: 指令回传配置。
  • 构建脚本依赖进 [build-dependencies]。

Q16. dev-dependencies / build-dependencies 和普通依赖差在哪?[patch] 何时用?

Tags: common dev-dependencies patch 适用版本: Cargo

一句话答案: 普通依赖进最终产物;dev-dependencies 只服务测试/示例/基准;build-dependencies 只服务 build.rs。[patch] 用于本地或临时替换依赖图里的某个 crate,不适合当长期“私服”。

解答: 三类依赖出现在不同编译目标里,乱放会导致测试专用 crate 被发布进库,或构建脚本工具被链进运行时。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
[dependencies]
serde = "1"                 # 正常编译、运行都需要

[dev-dependencies]
tempfile = "3"              # 仅 tests / examples / benches
assert_cmd = "2"

[build-dependencies]
cc = "1"                    # 仅 build.rs

[patch.crates-io]
# 临时用本地路径或 fork 覆盖 crates.io 上的同名 crate
regex = { path = "../regex" }

[patch] 常见场景:修上游 bug 时先挂 path/git fork、在 workspace 里调试尚未发布的版本。它改的是解析结果,不是新的依赖“种类”。发布到 crates.io 的库不能指望下游自动带上你的 patch;应用/workspace 内部自用更合适。长期私有源应配 registry/[source],而不是靠 patch 冒充。

Go 对比:

  • dev-dependencies 有点像只在测试里用的模块,但 Go 不单独在 go.mod 里划这么清晰的三段。
  • [patch] 类似 replace 指令:本地替换、临时覆盖。
  • 替换能救急,但要记得上游合并后删掉 patch。

记忆点:

  • deps / dev-deps / build-deps 服务不同阶段。
  • 测试专用库别写进 [dependencies]。
  • [patch] 是临时替换,不是正式依赖源。

Q17. 我没开某个 feature,为什么可选功能仍被编进?

Tags: hot features unification 适用版本: Cargo

一句话答案: 因为 Cargo 对同一 package 做 feature unification(feature 统一):依赖图里只要有一条边打开了该 feature,整图里这个 crate 的那一份就会带上它,不是“你本地没写就关着”。

解答: Feature 不是按“当前 crate 的 Cargo.toml 一行”单独编译多份再拼;同一 semver 兼容版本的 crate 在解析结果里通常只有一份,其启用 feature 是所有引用方的并集。所以你自己没写 features = ["foo"],只要某个传递依赖、workspace 成员、或默认 feature 间接打开了 foo,你也会编进对应代码。

1
2
3
4
5
6
# 你以为只依赖了“精简版”
[dependencies]
some-lib = "1"

# 但另一处(或传递依赖)打开了可选功能:
# other-crate = { version = "1", features = ["some-lib/extra"] }
1
2
3
# 先看谁打开了 feature,再决定关谁
cargo tree -e features
cargo tree -i some-lib

排查顺序:cargo tree -e features 找启用边 → 判断是默认 feature、直接依赖,还是传递依赖 → 再考虑 --no-default-features、换依赖版本,或让上游别无条件开 feature。互斥 feature(开了 A 就不能开 B)在统一规则下特别容易踩坑,设计库时尽量避免。

Go 对比:

  • Go 的 build tag / 条件编译更偏“按文件或按目标裁剪”,没有 Cargo 这种跨依赖图的 feature 并集。
  • 在 Rust 里“我没开”只说明你自己的 manifest;最终以整图统一结果为准。
  • 看到意外可选依赖进了二进制,先查 feature 图,别先猜编译器 bug。

记忆点:

  • Feature 按依赖图并集统一,不是按单个 Cargo.toml 隔离。
  • 排查入口:cargo tree -e features。
  • 和 Q8 的命令行开关配合看。

Q18. 改 edition 到底影响什么?改了会不会换语言语义?

Tags: common edition 适用版本: Cargo / Rust editions(2015 / 2018 / 2021 / 2024)

一句话答案: edition 决定本 package 的默认语法与若干语言默认行为;它不是工具链版本号,也不自动升级依赖的 edition。

解答: 写在 Cargo.toml 的 edition = "2021"(或 "2024" 等)影响的是:哪些语法默认可用、部分关键字预留、路径/async 等历史差异的默认解读。换 edition 通常要靠 cargo fix --edition 一类迁移辅助,再人工确认。它不会:把 rustc/cargo 升到新版本、改写依赖 crate 的 edition、或替你打开不稳定 feature。

1
2
3
4
[package]
name = "my-app"
version = "0.1.0"
edition = "2024"   # 只约束本 package 的语言 edition
1
2
rustc --version          # 工具链是另一回事
cargo fix --edition      # 迁移时常用;改完再 cargo check / test

依赖可以继续是 2018/2021,你的包用 2024:链接时各编各的,这是 edition 设计目标之一。想用新稳定 API,靠的是 Rust 版本 / MSRV(Minimum Supported Rust Version,最低支持的 Rust 版本),不是只改 edition 字符串。

Go 对比:

  • 有点像 Go 的 go 1.xx 语言版本行,但 Cargo 把“语言 edition”和“安装的编译器版本”分得更开。
  • 改 edition ≠ rustup update。
  • 团队应约定 edition 与 MSRV,避免“能编过的机器编不过”。

记忆点:

  • edition = 本包语言默认,不是工具链版本。
  • 依赖可混用不同 edition。
  • 新 API 看 Rust 版本 / MSRV,不单看 edition。

Q19. release 怎么瘦身?LTO 和 strip 怎么用?

Tags: common release LTO strip binary-size 适用版本: Cargo

一句话答案: 在 [profile.release] 里提高优化并做链接期裁剪:常用 lto(Link-Time Optimization,链接时优化)、codegen-units = 1、strip = true(或 "symbols"),必要时再加 panic = "abort";先测体积与性能,不要盲开全套。

解答: cargo build --release 默认已经比 dev 小且快,但还可继续压。lto = true(或 "thin")让链接阶段跨 crate 优化/内联,通常更小更快,编译更慢。strip 去掉符号表,对发布二进制很有效。codegen-units = 1 利于优化,代价是并行编译变差。

1
2
3
4
5
6
[profile.release]
opt-level = 3
lto = true              # 或 "thin":更快链接,体积收益通常略小
codegen-units = 1
strip = true            # 去掉调试符号;需要保留符号时用 "symbols" 等细选项
panic = "abort"         # 去掉 unwind 表,进一步瘦身(失去 panic 展开)
1
2
3
cargo build --release
# Windows 下看产物大小,路径随目标三元组变化
# target/release/<你的二进制名>.exe

注意:strip/lto 会让崩溃栈、性能剖析更难;调试用的符号别和发布制品混为一谈。库 crate 的体积问题更多在下游应用侧体现;应用/CLI 才是这套 profile 的主战场。依赖与 feature 裁剪(见 Q8、Q17)往往比再拧半档 LTO 更划算。

Go 对比:

  • Go 也有 -ldflags="-s -w" 一类去符号手段;Rust 把更多旋钮放进 Cargo profile。
  • 体积优化要兼顾编译时间和可调试性,和 Go 发布构建一样是工程权衡。
  • 先 cargo tree / 关无用 feature,再开 LTO。

记忆点:

  • 瘦身三件套:lto + strip +(可选)codegen-units = 1。
  • 先裁依赖/feature,再拧 profile。
  • 发布与调试 profile 分开想。

Q20. [workspace.dependencies] 是干什么的?成员怎么继承?

Tags: hot workspace dependencies 适用版本: Cargo workspace

一句话答案: 在 workspace 根把依赖版本(和常用 features)集中写一次;成员用 foo = { workspace = true } 继承,避免每个 crate 各写一套版本号。

解答: 多成员仓库最常见的痛是:serde 在 A 里是 1.0.190、B 里是 "1"、C 又开了不同 feature,升级时满仓搜。[workspace.dependencies] 把“用哪一版、默认开哪些 feature”收到根 Cargo.toml。

1
2
3
4
5
6
7
8
9
# 根 Cargo.toml
[workspace]
members = ["crates/core", "crates/cli"]
resolver = "3"

[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
anyhow = "1"
my-core = { path = "crates/core" }   # 成员之间互依也可放这里
1
2
3
4
5
6
7
# crates/cli/Cargo.toml
[dependencies]
serde = { workspace = true }                 # 继承版本 + 根里写的 features
anyhow = { workspace = true }
my-core = { workspace = true }
# 成员仍可再追加 features:
# serde = { workspace = true, features = ["rc"] }

注意:workspace = true 继承的是根里那一份声明;成员本地再写 version = ... 通常没必要,也容易和“单一真相源”打架。日常改版本优先改根,再 cargo check --workspace。和 Q9 的作用域命令、Q16 的 deps/dev-deps 分工是正交的:这里管的是版本集中,不是依赖种类。

Go 对比:

  • 有点像 Go workspace / 单模块里统一依赖版本的思路,但 Cargo 用显式 workspace = true 继承。
  • 别把 [workspace.dependencies] 当成“自动给所有成员装上”——成员仍要自己声明需要哪些 crate。
  • 升级依赖先改根,再全仓验证。

记忆点:

  • 版本写在根 [workspace.dependencies]。
  • 成员:{ workspace = true }。
  • 改一处,全仓对齐。

Q21. rust-version / MSRV 在 Cargo 工作流里怎么用?

Tags: common MSRV rust-version 适用版本: Cargo(rust-version 已稳定多年)

一句话答案: 在 [package] 写 rust-version 声明 MSRV(Minimum Supported Rust Version,最低支持的 Rust 版本);它主要是兼容承诺与工具提示,不是 rustup 自动切版本。细节与对照见 02-updating。

解答: 和 Q18 说的一样:edition ≠ 工具链版本。库/应用若承诺“至少支持到某版 rustc”,就在 manifest 里写清楚:

1
2
3
4
5
[package]
name = "my-lib"
version = "0.1.0"
edition = "2024"
rust-version = "1.85"

工作流上记住三件事:

  1. 声明:rust-version = MSRV 承诺。
  2. 开发锁链:团队日常用哪版 rustc,更常写在 rust-toolchain.toml(见 02-updating)。
  3. 验证:CI 应用 MSRV 工具链跑 cargo test(库作者尤其值得);resolver 3 还会在选依赖版本时更照顾 rust-version(见 22-packages-crates-and-modules 相关讨论)。

它不会在你本机低于 MSRV 时魔法升级编译器;低了就编不过或工具报警,该 rustup update / 换 toolchain。

Go 对比:

  • 类似 go.mod 里的 go 1.xx 语言/工具线提示,但 Rust 常把“最低支持”和“当前开发锁链”拆成 rust-version + rust-toolchain.toml。
  • 改 edition 解决不了“要用更新的标准库 API”。
  • 对外库写清 MSRV,对内用 toolchain 文件对齐团队。

记忆点:

  • rust-version = MSRV 声明。
  • 详解交叉引用 02-updating Q10。
  • 和 edition、toolchain 文件分开记。

Q22. cargo deny / cargo audit 是干什么的?CI 要不要加?

Tags: common cargo-deny cargo-audit CI security 适用版本: 外部 Cargo 子命令(cargo-audit / cargo-deny);与 rustc 版本无关

一句话答案: cargo audit 对照 RustSec 公告扫 Cargo.lock 里的已知漏洞;cargo deny 更宽——公告之外还可管许可证、禁用 crate、来源(crates.io/git)等策略。安全敏感或要发布的项目,CI 里至少加一类(常从 audit 或 deny 的 advisories 检查起步)。

解答: 两者都不是 rustup 自带的核心子命令,需另行安装:

1
2
cargo install cargo-audit
cargo install cargo-deny

日常用法示意:

1
2
3
4
5
# 扫锁文件中的已知漏洞 advisories
cargo audit

# 按仓库里的 deny.toml 做策略检查(许可证 / 公告 / bans / sources)
cargo deny check

cargo-deny 常配一份 deny.toml(示意,按团队改):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
[advisories]
yanked = "deny"

[licenses]
allow = ["MIT", "Apache-2.0", "BSD-3-Clause", "Unicode-3.0"]

[bans]
multiple-versions = "warn"

[sources]
unknown-registry = "deny"
unknown-git = "warn"

和 Q10 的 fmt / clippy / test 关系:那是正确性与风格基线;audit/deny 是供应链与合规层。CI 建议:

场景建议
玩具 / 周末脚本可先不加
团队服务 / 要上线加 cargo audit 或 cargo deny check
开源库 / 合规严deny(许可证 + 公告)更常见
锁文件常变与 --locked 测试同一 PR 跑,避免「测过的图」和「扫的图」不一致

误报或尚未升级的传递依赖:用工具自己的 ignore / 例外机制记原因,别整段关掉检查。

Go 对比:

  • Go 怎么做:govulncheck、依赖许可证扫描、私有代理策略。
  • Rust 为什么不同:生态标准答案常落在 cargo-audit / cargo-deny,而不是语言自带命令。
  • Go 程序员易踩的坑:以为 clippy 已覆盖 CVE——没有;漏洞库要单独扫。

记忆点:

  • audit ≈ 漏洞公告;deny ≈ 公告 + 许可证 + 策略。
  • CI:上线项目建议加;与 Q10 检查流水线并列,不互相替代。

Q23. 编译太慢:sccache / mold / lld 怎么加速?

Tags: hot sccache mold lld link CI 适用版本: 外部工具;平台相关

一句话答案: sccache(或 RUSTC_WRAPPER)缓存编译对象,适合 CI 与多 crate 工程;mold / lld 换更快链接器,常能明显缩短 release 链接时间。对标 Go 较少「换链接器」心智,但 Rust 大项目很常见。

解答:

1
2
3
# sccache 示意
cargo install sccache
# 环境变量:RUSTC_WRAPPER=sccache
1
2
3
4
5
6
7
8
# ~/.cargo/config.toml 示意
[build]
rustc-wrapper = "sccache"

# Linux 用 mold 示意
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
1
2
3
4
fn main() {
    // 先确认瓶颈是「编译」还是「链接」
    println!("cache compiles; fast linker links");
}

注意:链接器与 wrapper 是本机/CI 配置,一般不写进库的强制依赖。Windows 上按平台文档选 lld 等。

Go 对比:

  • Go 编译通常更快,较少谈 sccache;CI 仍有模块缓存。
  • Rust 为什么不同:单态化与 LLVM 更重,缓存/链接器收益大。
  • Go 程序员易踩的坑:只加 CPU、不改链接器,release 仍卡在 link。

记忆点:

  • 重复编译 → sccache。
  • release 链接慢 → mold/lld。

Q24. cargo nextest 和默认 cargo test 差在哪?

Tags: common nextest CI test-runner 适用版本: cargo-nextest 外部工具

一句话答案: cargo-nextest 是更快、隔离更好的测试运行器:默认每测一进程、调度更聪明、失败重跑与报告更友好。日常可用 cargo test;大仓库/CI 常换 nextest。

解答:

1
2
3
cargo install cargo-nextest
cargo nextest run
cargo nextest run --workspace
1
2
3
4
相对 cargo test 的常见卖点:
- 测试隔离(默认独立进程)
- 更好的并行调度与重试
- CI 友好的输出
1
2
3
4
fn main() {
    // 测试代码本身不用改:仍是 #[test]
    println!("same tests, different runner");
}

与 24-testing 正交:测什么怎么写在 24;用哪个 runner 在本题。

Go 对比:

  • 仍是 go test 为主;Rust 社区 nextest 很常见。
  • Go 程序员易踩的坑:以为必须改 #[test] 属性才能用 nextest。

记忆点:

  • 大项目 CI → 考虑 cargo nextest run。
  • 测试写法不变。
最后修改 August 11, 2026: 更新 (70a5af133)