手动输出宏
调试最简单粗暴的方式就是往控制台打印信息, 我们需要在代码的各个关键位置插入打印宏, 观察程序的中间状态
| 宏 | 作用 |
|---|---|
| dbg | 打印表达式的同时打印源码文件/行号 |
| println | 将内容输出到 stdout (换行) |
| eprintln | 将内容输出到 stderr (换行) |
println!/eprintln!/print!/eprint!以及格式化占位符{}/{:?}等详细内容, 已经在 1.常见宏.md 中介绍过, 这里不再赘述, 重点是dbg!
dbg! 宏
dbg! 是一个专门用于调试的宏, 和 println! 相比有以下特点:
- 自动带上源码文件路径和行号, 定位更加方便
- 输出到 stderr, 不会污染我们通过 stdout 输出的正常数据(例如管道传给其他程序的数据)
- 会返回传入表达式的值, 因此可以就地嵌入到表达式中(甚至可以链式使用)
- 会打印
文字名 = 值, 不需要手动补上变量名
提示
发布(Release) 版本中 dbg! 不会像 debug 断言那样被移除, 它会照常编译执行, 所以只把它用于开发期的临时调试, 不要留到正式代码里
| 方式 | 示例 |
|---|---|
| 单独打印表达式 | dbg!(x + 1) |
| 返回表达式值, 嵌入到赋值/表达式中 | let y = dbg!(x * 2); |
| 链式调用 | fn_t().dbg() / dbg!(dbg!(x)) |
fn main() {
let x = 42;
//=== 打印变量(自动带上 名字=值, 以及源码文件+行号) ===//
dbg!(x);
// [src/main.rs:4:5] x = 42
// 打印表达式
dbg!(x + 1);
// [src/main.rs:7:5] x + 1 = 43
//=== dbg! 会返回表达式的值, 可以就地嵌入 ===//
let y = dbg!(x * 2);
// [src/main.rs:11:13] x * 2 = 84
println!("y = {y}"); // y = 84
// 甚至可以链式使用
dbg!(dbg!(x / 2) + 1);
// [src/main.rs:16:7] x / 2 = 21
// [src/main.rs:16:12] dbg!(x / 2) + 1 = 22
// 打印数组等集合也很方便
let nums = [1, 2, 3, 4];
dbg!(nums);
// [src/main.rs:22:5] nums = [
// 1,
// 2,
// 3,
// 4,
// ]
// 注意: 这里利用数组是 Copy 的, 打印后数组依旧可用
// 如果是非 Copy 类型(比如 String), 直接 dbg!(s) 会「移动」(所有权转移)
let s = String::from("hello");
dbg!(&s); // 借用, 打印后还能继续使用
// [src/main.rs:29:5] &s = "hello"
println!("{s}"); // hello
}注意点
dbg! 对于传入的表达式是按值(所有权)传递的。如果传入的是非 Copy 类型(如 String, Vec), 直接 dbg!(s) 会发生移动, 后续就无法再使用 s 了, 需要借用 dbg!(&s) 才能继续使用
调整编译器输出信息
打印 panic 的调用栈 RUST_BACKTRACE
当程序发生 panic! 时, 默认只打印错误信息, 并不会显示调用栈(调用堆栈)。 如果想定位 panic 到底是从哪一层被传上来的, 可以设置环境变量 RUST_BACKTRACE:
| 取值 | 作用 |
|---|---|
1 | 显示基本的调用栈 |
full | 显示完整的调用栈(包含所有细节) |
| 未设置 | 默认只显示 panic 位置, 不打印调用栈 |
该内容在 30-错误处理专题.md 中也有涉及
# 运行时会先显示 panic 的位置, 再打印 stack backtrace
RUST_BACKTRACE=1 cargo run
# 输出 (节选):
# thread 'main' panicked at src/main.rs:4:5:
# 出错了
# stack backtrace:
# 0: rust_begin_unwind
# at /rustc/.../library/std/src/panicking.rs:...
# 1: core::panicking::panic_fmt
# 2: main
# 3: std::sys_common::backtrace::__rust_begin_short_backtrace
# ...提示
RUST_BACKTRACE 设置的环境变量只在本次命令生效; 如果想默认一直打印调用栈, 也可以在源码中用 std::env::set_var("RUST_BACKTRACE", "1"); 在最开始设置, 不过更推荐在开发调试时通过命令行临时设置
其他环境变量 RUST_LOG
RUST_LOG 用于控制日志库(log + env_logger)的输出等级, 在下面的"日志"一节会用到
日志
log 与 env_logger
标准库没有提供内置的日志组件, 通常使用第三方 crate 组合:
- log: 只提供日志门面(facade), 定义了日志的抽象
- env_logger: 是
log的一个具体实现, 负责真正把日志写出去
日志等级从高到低依次为(只会输出大于等于设定等级的日志):
| 等级宏 | 说明 |
|---|---|
error! | 错误, 最严重 |
warn! | 警告 |
info! | 信息 |
debug! | 调试 |
trace! | 跟踪, 最详细(默认不输出) |
[dependencies]
log = "0.4"
env_logger = "0.11"use log::{debug, error, info, trace, warn};
fn main() {
// 初始化日志器(这里用 env_logger 的默认配置)
env_logger::init();
trace!("some trace message");
debug!("some debug message");
info!("some info message");
warn!("some warn message");
error!("some error message");
}默认 env_logger 的过滤等级是 error, 也就是只输出 error!。 想要看到更多等级的日志, 通过环境变量 RUST_LOG 指定:
# 输出 error / warn / info / debug / trace 的全部日志
RUST_LOG=trace cargo run
# 只输出 error 和 warn
RUST_LOG=warn cargo run更深入的日志配置(输出到文件、自定义格式、按日期分割等), 请参考 03.日志处理.md
tracing
tracing 是比 log 更进一步的应用级日志/追踪库, 专门为异步任务设计, 可以记录调用跨度(spans)和事件, 常用于实现对请求链路的追踪。
[dependencies]
tracing = "0.1"
tracing-subscriber = "0.3"use tracing::{info, instrument, Level};
#[instrument]
async fn handle_request(path: &str) {
info!(path, "处理请求");
// ...
}
#[tokio::main]
async fn main() {
tracing_subscriber::fmt().with_max_level(Level::TRACE).init();
handle_request("/hello").await;
}关于日志与追踪的详细介绍可以参考 03.日志处理.md
IDE 与 Debugger
在 IDE 中利用图形化的调试器, 可以给代码打断点、单步执行、实时查看变量值, 比单纯的打印调试高效很多
VSCode + rust-analyzer + CodeLLDB
| 组件 | 作用 |
|---|---|
| VSCode | 编辑器 |
| rust-analyzer | Rust 语言服务, 提供补全、跳转、重构等智能提示 |
| CodeLLDB | LLDB 调试器扩展, 提供断点、单步、监视变量等图形化界面 |
使用步骤大致如下:
- 安装 VSCode, 并在扩展市场安装
rust-analyzer和CodeLLDB - 打开 Rust 项目,
rust-analyzer会自动加载并分析代码 - 在需要的地方点击行号左侧打上断点(红点)
- 按
F5选择LLDB调试配置, 启动调试 - 通过调试面板实现单步跳过(F10) / 步入(F11) / 步出(Shift+F11), 以及在左侧监听变量
提示
除了基于 LLDB 的调试器, Rust 社区也有使用 cargo-hack、Criterion 等工具做性能测试/基准测试的实践, 但这些的重点侧重于"性能分析"而非"调试程序"