Rust-开发应用-如何实现单例

前言

单例模式是一种创建型设计模式,其目的是确保一个类仅有一个实例,并且为该实例提供一个全局访问点。在实际工程中,我们会大量复用这种模式,用于配置表、中间件链接等场景。本文将说明如何在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,当然这又是一个方案上的取舍了:)

相关推荐
Ai拆代码的曹操2 小时前
Spring 事务 REQUIRES_NEW 嵌套调用:连接池翻倍的秘密
java·后端·spring
Ivanqhz3 小时前
Rust &‘static str浅析
java·前端·javascript·rust
IT_陈寒4 小时前
SpringBoot这个分页坑,我踩了三天才爬出来
前端·人工智能·后端
颜酱4 小时前
05 | 召回前置准备:根据业务数据库生成各数据库(读取配置阶段)
前端·人工智能·后端
Wang's Blog4 小时前
Go-Zero项目开发24: 基于Bitmap实现群聊消息已读未读
开发语言·后端·golang
Conan在掘金5 小时前
鸿蒙报错速查:struct 里嵌 namespace 声明就炸,根因 + 真解法
后端
东方小月5 小时前
从零开发一个 Coding Agent(三):EventStream 事件流通道设计与实现
前端·人工智能·后端
卫子miao5 小时前
如何评价当前大语言模型的记忆机制?
后端·架构
神奇小汤圆5 小时前
为什么 AQS 成为 Java 并发的基石?
后端
星栈6 小时前
MCP 从 stdio 迁到 SSE,踩了 5 个传输层坑
人工智能·后端·架构