Frontend Labs
⋯

3. Variables, mutability, shadowing, and scalar types

3.1 Immutable by default

fn main() {
    let x = 5;          // immutable binding
    // x = 6;           // error[E0384]: cannot assign twice to immutable variable
    let mut y = 5;      // `mut` is opt-in, and greppable
    y += 1;

    let x = x * 2;      // SHADOWING: a brand-new variable that hides the old `x`
    let spaces = "   ";
    let spaces = spaces.len(); // shadowing can even change the type: &str -> usize

    println!("{x} {y} {spaces}"); // 10 6 3
}

mut belongs to the binding, not to the type. let mut v = Vec::new() means "this variable may be reassigned or mutated through". Later you will see the same distinction between &T and &mut T.

3.2 Scalar types, and the sizes that are actually guaranteed

Rust Size Go C
i8 i16 i32 i64 i128 exact int8 … int64 int8_t … (<stdint.h>)
u8 u16 u32 u64 u128 exact uint8 … uint64 uint8_t …
isize usize pointer width int uint ptrdiff_t size_t
f32 f64 IEEE 754 float32 float64 float double
bool 1 byte, only 0 or 1 bool _Bool
char 4 bytes, a Unicode scalar value rune — (char is a byte)

Unlike C §3.1, every integer type has a fixed size. Indexes and lengths are always usize, so you will write as usize often at first.

3.3 Integer overflow: defined, but different in debug and release

In C, signed overflow is UB. In Go, it silently wraps. Rust's behavior is defined in every case, and you choose it:

fn main() {
    let a: u8 = 250;

    // In a DEBUG build, `a + 10` panics: "attempt to add with overflow".
    // In a RELEASE build it wraps to 4 (two's complement), unless you set
    // `overflow-checks = true` in [profile.release].
    // When overflow matters, say what you want explicitly:
    println!("{:?}", a.checked_add(10));     // None
    println!("{}", a.wrapping_add(10));      // 4
    println!("{}", a.saturating_add(10));    // 255
    println!("{:?}", a.overflowing_add(10)); // (4, true)

    // Conversions are explicit. `as` truncates silently; `try_from` checks.
    let big: i64 = 300;
    let truncated = big as u8;                    // 44, silently
    let checked = u8::try_from(big);              // Err(TryFromIntError(()))
    println!("{truncated} {checked:?}");
}

Real-world example. Parsing a length field from a network packet (Ch 45): let len = u16::from_be_bytes([b0, b1]) as usize; is safe because u16 → usize can never lose data. Going the other way (usize → u16 when writing a packet) must use u16::try_from(len)?. With as, a 70,000-byte payload would quietly be encoded as a 4,464-byte length, which is the classic framing bug behind many C parser CVEs.

Production setting: many security-sensitive projects set overflow-checks = true in [profile.release]. The cost is usually under a few percent. Profile your own workload before deciding.

3.4 Tuples, arrays, and type inference

fn main() {
    let pair: (i32, &str) = (200, "OK");
    let (code, reason) = pair;               // destructuring
    println!("{code} {reason} {}", pair.0);

    let a = [0u8; 4];                        // [u8; 4], four zero bytes, on the stack
    let b = [1, 2, 3];                       // inferred [i32; 3]
    println!("{} {:?} {}", a.len(), b, b[2]);
    // b[3] would not compile here: the index is a constant, and the compiler knows the length.
    // For a runtime index, an out-of-bounds access PANICS. It never reads past the end.
    let i = std::env::args().count() + 5;
    println!("{:?}", b.get(i));             // None: the non-panicking accessor
}

Bounds checks are the reason Rust programs cannot have the C guide's §29 stack overflow. They usually cost little, because LLVM removes them when it can prove the index is in range. Iterators (Ch 21) avoid them entirely.


Generated from real-life-rust-guide.md · 8 Oct 2026 16:36