Right, let’s talk about static. This is where we graduate from playing in the sandbox to juggling chainsaws. It’s incredibly powerful, occasionally necessary, and if you get it wrong, the resulting segfault will feel deeply personal. A static variable is essentially a global variable. I know, I know. You’ve heard horror stories about global state. Those stories are true. But in systems programming, sometimes you need a single, shared resource, and Rust, being the brilliant control freak it is, gives you a way to do it that’s almost safe. The key is the 'static lifetime.

The 'static lifetime is the big boss of lifetimes. It means the reference is valid for the entire duration of the program. A string literal like "Hello" is &'static str because it’s baked directly into the executable’s data segment. It’s always there, waiting for you.

Declaring and Using static

You declare a static variable with the static keyword. The critical thing to remember is that every static must be initialized with a constant expression. The compiler needs to know its value at compile time so it can set it up before your main function even dreams of running.

// A simple, immutable static string
static TITLE: &'static str = "My Excellent Program";

// A static counter. We'll get to the mutability horror in a second.
static COUNTER: i32 = 0;

fn main() {
    println!("Welcome to {}", TITLE);
    // This would fail: COUNTER += 1;
    // error: cannot assign to immutable static item `COUNTER`
}

See? By default, static items are immutable. This is Rust’s first line of defense against your own bad ideas. An immutable global is mostly harmless. It’s just a constant with a fancy, memory-fixed address.

The static mut Abyss

Now, let’s open Pandora’s box. If you need a mutable global, you must use static mut. The language makes this incredibly ugly and dangerous on purpose to scare you into finding a better way.

static mut DANGER_ZONE: i32 = 0;

fn main() {
    // This is a compilation error. Thank goodness.
    DANGER_ZONE += 1;
    // error: use of mutable static requires unsafe block
}

The compiler stops you dead in your tracks. To touch a static mut, you must do it inside an unsafe block. This is Rust’s way of saying, “Fine, you want to play with fire? I’m not going to stop you, but you have to wear this giant neon ‘IDIOT’ sign while you do it.” You are now solely responsible for ensuring no data races occur.

use std::thread;

static mut HIT_COUNT: i32 = 0;

fn main() {
    let thread_a = thread::spawn(|| {
        for _ in 0..100000 {
            unsafe { HIT_COUNT += 1; }
        }
    });

    let thread_b = thread::spawn(|| {
        for _ in 0..100000 {
            unsafe { HIT_COUNT += 1; }
        }
    });

    thread_a.join().unwrap();
    thread_b.join().unwrap();

    unsafe {
        println!("Final count: {}", HIT_COUNT); // Almost certainly NOT 200000
    }
}

This code is a data race waiting to happen. The two threads are mutating the same location without any synchronization. The final count will be a random number. This is bad. This is why we avoid static mut.

The Safe Alternative: std::sync::Mutex

You almost never want raw static mut. What you actually want is a safe, synchronized global. This is where we bring in the cavalry: Mutex, RwLock, and Atomic types. Wrapping your data in one of these and then making that the static is the correct pattern.

But here’s the catch: how do you initialize a Mutex::new(0) at compile time? You can’t, because Mutex::new is a function call, not a constant expression. This is where lazy initialization comes in, and the community has embraced the once_cell crate (and its stable descendant, std::sync::LazyLock) for this. But for a long time, the standard way was a bit of a hack using lazy_static!.

// You'd need to add `lazy_static = "1.4"` to your Cargo.toml
use lazy_static::lazy_static;
use std::sync::Mutex;

lazy_static! {
    static ref SAFE_COUNTER: Mutex<i32> = Mutex::new(0);
}

fn main() {
    let mut thread_handles = vec![];
    
    for _ in 0..10 {
        let handle = thread::spawn(|| {
            for _ in 0..10000 {
                let mut lock = SAFE_COUNTER.lock().unwrap();
                *lock += 1;
            }
        });
        thread_handles.push(handle);
    }
    
    for handle in thread_handles {
        handle.join().unwrap();
    }
    
    println!("Final count: {}", SAFE_COUNTER.lock().unwrap()); // This will be 100000
}

The lazy_static! macro creates a type that implements Deref to your Mutex<i32>. It handles the initialization for you on first access in a thread-safe way. This is the right way to do it. It’s safe, it’s synchronized, and it doesn’t require unsafe blocks littering your beautiful code.

const vs. static

This trips people up. Use const when you want a value that is inlined everywhere it’s used. It has no memory address. Use static when you want a variable with a fixed memory address. If you need to take a reference to something global, it must be static. If you just want a named constant for readability and maintenance, const is usually what you want.

const MAX_USERS: usize = 100; // Inlined. No address.
static LOG_FILE_PATH: &str = "/var/log/myapp.log"; // Has a unique address.

fn main() {
    // These might look the same...
    let a = MAX_USERS;
    let b = LOG_FILE_PATH;

    // ...but this is only possible for the `static`
    let address_of_log_path = &LOG_FILE_PATH as *const _;
}

In summary, static is your tool for global state. Tread carefully. Avoid static mut like it owes you money. Instead, reach for synchronized types like Mutex paired with a lazy initialization pattern. It’s a few more lines of code that buy you an immense amount of safety and sleep.