Loading pageā¦
Rust walkthroughs
Loading pageā¦
once_cell::sync::Lazy and lazy_static! macro for static initialization?Lazy static initialization defers the creation of values until first access, avoiding the overhead of startup initialization and enabling runtime-initialized statics. Both once_cell::sync::Lazy and the lazy_static! macro solve this problem, but with different ergonomics, capabilities, and integration patterns.
Rust's static initialization is constrained to compile-time constants:
// This works: compile-time constant
static MAX_CONNECTIONS: usize = 100;
// This fails: HashMap needs runtime initialization
static CONFIG: HashMap<String, String> = HashMap::new();
// Error: calls in constants are limited to constant functions
// This fails: function call at static initialization
static TIMESTAMP: String = format!("{}: {}", chrono::Utc::now(), "started");
// Error: calls in constants are limited to constant functionsLazy initialization solves this by deferring the initialization to first access.
The lazy_static! macro wraps initialization in a one-time execution block:
use lazy_static::lazy_static;
use std::collections::HashMap;
lazy_static! {
static ref CONFIG: HashMap<&'static str, &'static str> = {
let mut m = HashMap::new();
m.insert("host", "localhost");
m.insert("port", "8080");
m
};
static ref TIMESTAMP: String = {
format!("Started at: {}", chrono::Utc::now().format("%Y-%m-%d %H:%M:%S"))
};
}
fn main() {
// First access initializes CONFIG
println!("Host: {}", CONFIG.get("host").unwrap());
// Subsequent accesses use cached value
println!("Port: {}", CONFIG.get("port").unwrap());
}The macro generates a unique type implementing Deref, hiding the synchronization details.
once_cell::sync::Lazy provides a type that wraps initialization logic:
use once_cell::sync::Lazy;
use std::collections::HashMap;
static CONFIG: Lazy<HashMap<&'static str, &'static str>> = Lazy::new(|| {
let mut m = HashMap::new();
m.insert("host", "localhost");
m.insert("port", "8080");
m
});
static TIMESTAMP: Lazy<String> = Lazy::new(|| {
format!("Started at: {}", chrono::Utc::now().format("%Y-%m-%d %H:%M:%S"))
});
fn main() {
// First access initializes
println!("Host: {}", CONFIG.get("host").unwrap());
// Lazy<T> implements Deref<Target = T>
let timestamp: &String = &*TIMESTAMP;
}Lazy<T> is a real type you can work with directly, not a macro-generated wrapper.
The macro syntax can feel unusual compared to normal Rust declarations:
use lazy_static::lazy_static;
// Macro syntax: unusual static ref form
lazy_static! {
static ref VALUE: i32 = compute_value();
}
// vs normal static syntax
static NORMAL_VALUE: i32 = 42;Lazy uses standard Rust syntax:
use once_cell::sync::Lazy;
// Standard static syntax with Lazy type
static VALUE: Lazy<i32> = Lazy::new(|| compute_value());
// Can even use the simpler form with const initialization
static SIMPLE: Lazy<i32> = Lazy::new(|| 42);This consistency with normal Rust makes Lazy more approachable and IDE-friendly.
Lazy<T> is a concrete type, enabling patterns impossible with the macro:
use once_cell::sync::Lazy;
use std::collections::HashMap;
// You can name the type in function signatures
fn get_config() -> &'static Lazy<HashMap<String, String>> {
&CONFIG
}
// You can store Lazy in structs
struct AppState {
config: &'static Lazy<Config>,
database: &'static Lazy<DatabasePool>,
}
static CONFIG: Lazy<HashMap<String, String>> = Lazy::new(|| {
HashMap::new()
});
static DATABASE: Lazy<DatabasePool> = Lazy::new(|| {
DatabasePool::connect()
});With lazy_static!, you cannot name the generated type:
use lazy_static::lazy_static;
use std::collections::HashMap;
lazy_static! {
static ref CONFIG: HashMap<String, String> = HashMap::new();
}
// Cannot write this type: the macro generates an anonymous type
// fn get_config() -> &'static ??? { &CONFIG }Lazy provides access to initialization status:
use once_cell::sync::Lazy;
static VALUE: Lazy<i32> = Lazy::new(|| {
println!("Initializing!");
42
});
fn main() {
// Check if initialized without triggering initialization
if !Lazy::initialized(&VALUE) {
println!("Not yet initialized");
}
// Force initialization explicitly
Lazy::force(&VALUE);
// Check again
if Lazy::initialized(&VALUE) {
println!("Now initialized");
}
}lazy_static! provides no such introspectionāthe first Deref call always initializes.
Both approaches support mutable statics with proper synchronization:
use lazy_static::lazy_static;
use std::sync::Mutex;
lazy_static! {
static ref COUNTER: Mutex<i32> = Mutex::new(0);
}
use once_cell::sync::Lazy;
use std::sync::Mutex;
static COUNTER: Lazy<Mutex<i32>> = Lazy::new(|| Mutex::new(0));
// Both require explicit locking
fn increment() {
let mut count = COUNTER.lock().unwrap();
*count += 1;
}For simpler cases, once_cell provides race::OnceBox for atomic lazy initialization:
use once_cell::race::OnceBox;
static VALUE: OnceBox<i32> = OnceBox::new();
fn get_value() -> &'static i32 {
VALUE.get_or_init(|| Box::new(compute_expensive_value()))
}Lazy<T> works naturally in generic contexts:
use once_cell::sync::Lazy;
use std::collections::HashMap;
use std::hash::Hash;
use std::fmt::Debug;
struct Cache<K, V>
where
K: Eq + Hash + Debug + 'static,
V: Debug + 'static,
{
data: Lazy<HashMap<K, V>>,
}
impl<K, V> Cache<K, V>
where
K: Eq + Hash + Debug + 'static,
V: Debug + 'static,
{
fn new() -> Self {
Cache {
data: Lazy::new(HashMap::new),
}
}
}lazy_static! cannot be used in generic contexts because statics must have concrete types.
once_cell provides additional types for lazy initialization patterns:
use once_cell::sync::{Lazy, OnceCell};
use std::thread;
// OnceCell: Manual lazy initialization
static MANUAL: OnceCell<String> = OnceCell::new();
fn init_manual(value: String) {
// Only succeeds once; returns Err if already initialized
MANUAL.set(value).expect("Already initialized");
}
fn get_manual() -> &'static str {
MANUAL.get().map(|s| s.as_str()).unwrap_or("not initialized")
}
// Lazy: Automatic lazy initialization
static AUTO: Lazy<String> = Lazy::new(|| {
"automatically initialized".to_string()
});
// unsync::Lazy for single-threaded contexts
use once_cell::unsync::Lazy as UnsyncLazy;
fn single_threaded() {
let lazy: UnsyncLazy<i32> = UnsyncLazy::new(|| expensive_computation());
println!("Value: {}", *lazy);
}
fn expensive_computation() -> i32 {
42
}The lazy_static! crate provides only the macro with thread-safe semantics.
Both approaches use similar synchronization primitives internally:
// Both use atomic operations for the initialization check
// Both block competing threads during initialization
// Lazy initialization check is very fast after first init:
// - One atomic load (relaxed)
// - One pointer dereferenceThe overhead after initialization is minimalāa single atomic load. Initialization itself requires synchronization.
once_cell's patterns have been incorporated into the standard library:
// Rust 1.70+: std::sync::OnceLock (similar to once_cell::sync::OnceCell)
use std::sync::OnceLock;
static CONFIG: OnceLock<HashMap<String, String>> = OnceLock::new();
fn get_config() -> &'static HashMap<String, String> {
CONFIG.get_or_init(|| {
let mut m = HashMap::new();
m.insert("key".to_string(), "value".to_string());
m
})
}
// Rust 1.80+: std::sync::LazyLock (similar to once_cell::sync::Lazy)
use std::sync::LazyLock;
static VALUE: LazyLock<i32> = LazyLock::new(|| expensive_init());
fn expensive_init() -> i32 {
42
}The standard library types are nearly identical to once_cell types, making the external crate optional for newer Rust versions.
Migrating is straightforward:
// Before: lazy_static
use lazy_static::lazy_static;
lazy_static! {
static ref CONFIG: HashMap<&'static str, String> = {
let mut m = HashMap::new();
m.insert("host", "localhost".to_string());
m
};
}
// After: once_cell
use once_cell::sync::Lazy;
static CONFIG: Lazy<HashMap<&'static str, String>> = Lazy::new(|| {
let mut m = HashMap::new();
m.insert("host", "localhost".to_string());
m
});
// Or with std (Rust 1.80+)
use std::sync::LazyLock;
static CONFIG: LazyLock<HashMap<&'static str, String>> = LazyLock::new(|| {
let mut m = HashMap::new();
m.insert("host", "localhost".to_string());
m
});once_cell::sync::Lazy or std::sync::LazyLock when:// You need type visibility
static CACHE: Lazy<Cache> = Lazy::new(Cache::new);
fn get_cache() -> &'static Lazy<Cache> { &CACHE }
// You need to check initialization status
if Lazy::initialized(&CACHE) { /* ... */ }
// You want standard Rust syntax
static VALUE: Lazy<i32> = Lazy::new(|| compute());
// You're in a generic context
struct Container<T> {
cache: Lazy<HashMap<String, T>>,
}
// You want std compatibility (Rust 1.80+)
use std::sync::LazyLock;
static VALUE: LazyLock<String> = LazyLock::new(|| "value".to_string());lazy_static! when:// Maintaining legacy code that already uses it
lazy_static! {
static ref CONFIG: HashMap<&'static str, &'static str> = load_config();
}
// You prefer the macro syntax for multiple statics
lazy_static! {
static ref DB: Database = Database::connect();
static ref CACHE: Cache = Cache::new();
static ref METRICS: Metrics = Metrics::new();
}Both approaches panic on initialization failure by default, but once_cell offers alternatives:
use once_cell::sync::OnceCell;
use std::io;
static MAYBE_CONFIG: OnceCell<Result<Config, io::Error>> = OnceCell::new();
fn get_config() -> Result<&'static Config, &'static io::Error> {
MAYBE_CONFIG
.get_or_init(|| Config::load())
.as_ref()
.map(|c| *c)
.map_err(|e| e)
}
// With lazy_static, panics are the only option
use lazy_static::lazy_static;
lazy_static! {
static ref CONFIG: Config = Config::load().expect("Failed to load config");
}once_cell::sync::Lazy and lazy_static! both provide thread-safe lazy static initialization with similar runtime performance. The key differences are ergonomic and structural:
Lazy<T> is a concrete type that integrates naturally with Rust's type system. You can name it in function signatures, store it in structs, check initialization status, and use it in generic contexts. It uses standard Rust syntax, making it more approachable and IDE-friendly.
lazy_static! uses a macro that generates an anonymous type, which works well for simple cases but limits flexibility. The macro syntax groups multiple statics together, which some developers prefer for organization.
For new code, once_cell::sync::Lazy (or std::sync::LazyLock in Rust 1.80+) is generally preferred due to better type integration and standard library alignment. The lazy_static! macro remains useful for legacy codebases and for developers who prefer its grouping syntax for multiple related statics.
Both approaches have minimal overhead after initializationāa single atomic loadāand both ensure thread-safe one-time initialization. The choice is primarily about ergonomics and type system integration rather than performance.