Statics

#120 May 2026

120. OnceLock — Lazy Statics That Initialize on Your Schedule

LazyLock runs its initializer the first time anyone touches the value — fine when the inputs are baked in at compile time, useless when you only learn them at runtime. OnceLock is the same idea, but you decide when (and with what data) initialization happens.

The classic case: you want a global that’s expensive to build, and the data only exists after main starts — CLI args, env vars, a parsed config file. LazyLock can’t see those without baking the work into a closure that re-runs every test setup.

OnceLock solves it by separating creation from initialization:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
use std::sync::OnceLock;

static CONFIG: OnceLock<String> = OnceLock::new();

fn main() {
    // Initialize from real runtime data, exactly once.
    let cfg = std::env::var("APP_CONFIG").unwrap_or_else(|_| "default".into());
    CONFIG.set(cfg).unwrap();

    assert_eq!(config(), "default");
}

fn config() -> &'static str {
    CONFIG.get().expect("config not initialized")
}

set returns Err if the cell was already filled — you get to decide whether that’s a panic, a log line, or a no-op.

For the read-mostly path, get_or_init combines “is it set?” and “set it” into a single thread-safe call. Concurrent callers race; the winner runs the closure, everyone else waits and reads the result:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
use std::sync::OnceLock;

static GREETING: OnceLock<String> = OnceLock::new();

fn greeting() -> &'static str {
    GREETING.get_or_init(|| format!("hello, {}", "world"))
}

assert_eq!(greeting(), "hello, world");
assert_eq!(greeting(), "hello, world"); // cached, closure does not run again

When to reach for which: pick LazyLock when the initializer is self-contained and you’re happy with it firing on first touch. Pick OnceLock when you need to feed in runtime data — or when you want the option to ask “has this been set yet?” before triggering the work.