Introduction: Why is a Global Singleton in Rust a Challenge?
\n\nIf you come from the world of Python, JavaScript, or C++, you are used to a simple global object. Declare a variable at the module level — and it's yours. In Rust, such a trick won't work. The compiler strictly monitors the lifetime and access to data, especially if it can be changed from different threads. That is why on Stack Overflow the question «How do I create a global, mutable singleton?» has gathered thousands of views and dozens of complex answers.
\n\nBut don't rush to be scared. A singleton in Rust is not pain, but discipline. Today we will break down how to create a global mutable singleton safely, efficiently, and with an understanding of what's happening. You will learn why static mut is evil, and what tools (lazy_static, OnceCell, Mutex) give us control.
The article is written for the educational website pythonlib.ru and adapts a real SO question into a practical lesson. Let's go!
\n\nThe Problem: What is a Global Mutable Singleton?
\n\nSingleton is a pattern where a class or structure has exactly one instance throughout the entire program. In the context of Rust, we are talking about a global variable accessible from anywhere in the code that can be mutated (mut).
\n\nUsage examples:
\n- \n
- Application configuration loaded once at startup. \n
- Global logger or event counter. \n
- Database connection pool. \n
- In-memory cache. \n
The problem is that Rust forbids global mutable variables without a special wrapper. Why? Because they lead to data races and undefined behavior. The compiler cannot prove that two threads will not write to the same memory cell simultaneously.
\n\nThe naive solution looks like this:
\n// BAD code — don't do this!\nstatic mut COUNTER: u32 = 0;\n\nfn increment() {\n unsafe {\n COUNTER += 1;\n }\n}\n\nThis works, but requires unsafe and leaves you alone with races. In a multithreaded environment, it's a ticking time bomb. We need a safe, idiomatic approach.
Tool 1: lazy_static + Mutex — The Classic Combo
\n\nThe most popular way to create a global mutable singleton is the combination of lazy_static (lazy initialization) and Mutex (mutual exclusion).
\n\nAdd dependencies to Cargo.toml:
[dependencies]\nlazy_static = "1.4"\n\n\nNow the code:
\nuse lazy_static::lazy_static;\nuse std::sync::Mutex;\n\nlazy_static! {\n static ref GLOBAL_CONFIG: Mutex<Config> = Mutex::new(Config::default());\n}\n\n#[derive(Debug)]\nstruct Config {\n db_url: String,\n max_connections: u32,\n}\n\nimpl Default for Config {\n fn default() -> Self {\n Config {\n db_url: "localhost:5432".to_string(),\n max_connections: 10,\n }\n }\n}\n\nfn main() {\n // Reading\n let config = GLOBAL_CONFIG.lock().unwrap();\n println!("Current URL: {}", config.db_url);\n\n // Writing\n // drop(config); // explicitly release the lock\n let mut config = GLOBAL_CONFIG.lock().unwrap();\n config.db_url = "new-host:8888".to_string();\n println!("New URL: {}", config.db_url);\n}\n\n\nHow does it work?
\n- \n
lazy_static!guarantees that initialization happens only once upon first access. \n Mutex<T>provides mutual exclusion: only one thread can access the data at any given time. \n - The
.lock()method returns aMutexGuard, which automatically releases the lock when it goes out of scope. \n
Pros: simplicity, battle-tested, safe for