前言
单例模式是一种创建型设计模式,其目的是确保一个类仅有一个实例,并且为该实例提供一个全局访问点。在实际工程中,我们会大量复用这种模式,用于配置表、中间件链接等场景。本文将说明如何在Rust中实现单例。
场景
我们用如下测试用例模拟实际工程中的两个测试用例,为了方便调试我们需要在用例中初始化tracing_subscriber
才能记录程序的日志。
rust
#[cfg(test)]
mod tests {
#[tokio::test]
async fn test_1() {
tracing_subscriber::fmt::init();
tracing::info!("Running test 1");
assert_eq!(2 + 2, 4);
tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
tracing::info!("Test 1 completed");
}
#[tokio::test]
async fn test_2() {
tracing_subscriber::fmt::init();
tracing::info!("Running test 2");
assert_eq!(3 + 3, 6);
tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
tracing::info!("Test 2 completed");
}
}
运行测试,你会发现报如下错误:
shell
Unable to install global subscriber: SetGlobalDefaultError("a global default trace dispatcher has already been set")
原因是我们在两个测试用例当中都通过tracing_subscriber::fmt::init()
设置了一个全局的default trace dispatcher
。我们将通过单例来解决这个问题,需注意本文解法仅是为了演示如何实现单例,在实际工程实践中你则可以考虑通过test_log
等方式解决:)
lazy_static
在Rust@1.70之前,我们通常会使用lazy_static
来解决这个问题。lazy_static
是用于在Rust中声明延迟求值静态变量的宏。使用它,可以拥有需要在运行时执行代码才能初始化的静态变量,也就是懒加载的能力。使用lazy_static
你需要在你的项目中添加依赖:
toml
[dependencies]
lazy_static = "1.5.0"
我们的代码如下:
rust
#[cfg(test)]
mod tests {
lazy_static::lazy_static! {
static ref G_TRACING: std::sync::Arc<()> = std::sync::Arc::new({
tracing_subscriber::fmt::init();
});
}
#[tokio::test]
async fn test_1() {
let _tracing = G_TRACING.clone();
tracing::info!("Running test 1");
assert_eq!(2 + 2, 4);
tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
tracing::info!("Test 1 completed");
}
#[tokio::test]
async fn test_2() {
let _tracing = G_TRACING.clone();
tracing::info!("Running test 2");
assert_eq!(3 + 3, 6);
tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
tracing::info!("Test 2 completed");
}
}
使用Arc
使我们创建的静态变量能够被clone
。使用G_TRACING
我们就可以保证该方法仅会在第一次执行时被运行。
std
如果我们现在查看lazy_static
的crates.io,那么会发现其主页有这样一段描述:
It is now possible to easily replicate this crate's functionality in Rust's standard library with std::sync::OnceLock
.
当前开发者已经可以通过使用Rust标准库中的std::sync::OnceLock
来复用lazy_static
的功能(准确说应该是在Rust@1.70版本之后)。
我们可以通过以下代码来实现上文中的功能:
rust
#[cfg(test)]
mod tests {
static G_TRACING: std::sync::OnceLock<()> = std::sync::OnceLock::new();
fn g_tracing() -> &'static () {
G_TRACING.get_or_init(|| {
tracing_subscriber::fmt::init();
})
}
#[tokio::test]
async fn test_1() {
g_tracing();
tracing::info!("Running test 1");
assert_eq!(2 + 2, 4);
tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
tracing::info!("Test 1 completed");
}
#[tokio::test]
async fn test_2() {
g_tracing();
tracing::info!("Running test 2");
assert_eq!(3 + 3, 6);
tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
tracing::info!("Test 2 completed");
}
}
官方文档介绍中,std::sync::OnceLock
是线程安全版的std::cell::OnceCell
,因此如果不需要考虑线程安全,你也可以使用OnceCell
来实现你想要的功能。
最终运行结果:
bash
running 2 tests
2025-07-07T12:21:31.703227Z INFO learn_rust::tests: Running test 2
2025-07-07T12:21:31.703226Z INFO learn_rust::tests: Running test 1
2025-07-07T12:21:41.709084Z INFO learn_rust::tests: Test 1 completed
2025-07-07T12:21:41.709084Z INFO learn_rust::tests: Test 2 completed
test tests::test_1 ... ok
test tests::test_2 ... ok
successes:
successes:
tests::test_1
tests::test_2
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 10.01s
附上项目的cargo.toml
:
toml
[package]
name = "learn_rust"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
tokio = { version = "1.0", features = ["full"] }
tracing = "0.1"
tracing-subscriber = "0.3"
lazy_static = "1.4"
[dev-dependencies]
总结
本文介绍了如何在Rust中实现单例;一般来说,在新项目中你完全可以使用标准库提供的能力。但考虑到一些历史项目需考虑版本兼容性,你可能仍然需要使用lazy_static
,当然这又是一个方案上的取舍了:)