Syntax and ownership

Bindings, the primitive type set, and the ownership and borrowing rules that replace a garbage collector.

Values, bindings and functions

Bindings are immutable unless you write mut. The compiler infers types where it can and requires an annotation where it cannot.

fn main() {
    let count = 3;              // immutable by default
    let mut total = 0;          // mut allows reassignment
    total += count;

    let ratio: f64 = 0.75;      // annotation when inference is not enough
    let pair = (1, "two");      // tuple: fixed size, mixed types
    let (a, b) = pair;          // destructuring

    println!("{} {} {} {:?}", total, ratio, a, pair.1);
}

fn largest(list: &[i32]) -> i32 {
    let mut max = list[0];
    for n in list {
        if *n > max {
            max = *n;
        }
    }
    max
}
TypeNotes
i32, u64, usizeIntegers; i32 is the default, usize for indexing
f64Floating point; the default
booltrue or false
charOne Unicode scalar value, written with single quotes
StringOwned, growable, heap-allocated text
&strA borrowed view of text; no ownership, no allocation
Vec<T>Growable array, Vec::new() or vec![1, 2, 3]
Option<T>Some(T) or None; there is no null
💡
println! is a macro, not a function — that is what the ! means. {} uses the Display trait and {:?} uses Debug, which is what you get for free from #[derive(Debug)].

Ownership and borrowing

fn main() {
    let s = String::from("hello");
    let t = s;                 // move: s is no longer usable
    println!("{}", t);

    let u = t.clone();         // explicit deep copy when you need both
    println!("{} {}", t, u);

    let len = measure(&t);     // borrow instead of moving
    println!("{} has {} bytes", t, len);
}

fn measure(s: &str) -> usize {
    s.len()
}

fn push_word(v: &mut Vec<String>) {
    v.push(String::from("word"));
}

// lifetimes are usually elided; name them when the output borrows an input
fn first<'a>(words: &'a [String]) -> &'a str {
    words[0].as_str()
}
  • Every value has exactly one owner, and when the owner goes out of scope the value is dropped — that is the whole memory management strategy, with no garbage collector.
  • Assignment moves heap types such as String and Vec<T>, and copies simple Copy types such as i32 and bool.
  • A shared borrow &T allows read access without a move; a mutable borrow &mut T grants exclusive write access.
  • You may have any number of shared borrows or exactly one mutable borrow, never both at the same time. That single rule removes data races at compile time.
  • The borrow checker rejects the program, not the runtime: if it compiles, the reference it hands out is still valid when it is used.

FAQ

Why does the compiler say 'value borrowed here after move'?
Assignment moved the String into the new binding, so the old name is dead. Pass a reference with &s, or call .clone() when both bindings really need to own the data.
When is clone the right answer?
When two owners genuinely need independent copies, or when a borrow would force lifetimes through an API that does not care about them. It is a real allocation, so in a hot loop it is worth restructuring first.

Structs, enums and pattern matching Error handling with Result and Cargo

Last refreshed 2026-09-18.