Rust Beginner
The fundamentals — variables, ownership, control flow, functions, structs, enums, and your first taste of error handling.
Hello, Cargo
01Rust is installed via rustup. cargo is the build tool and package manager — you'll use it constantly.
$ cargo new hello
$ cd hello
$ cargo run
Compiling hello v0.1.0
Finished `dev` profile in 0.42s
Running `target/debug/hello`
Hello, world!
fn main() {
println!("Hello, world!");
}
Variables & Types
02Variables are immutable by default. Use mut to allow mutation. Types are usually inferred but can be annotated.
let name = "Ada"; // &str, immutable
let age: u32 = 36; // explicit type
let mut score = 0; // mutable
score += 10;
const MAX_USERS: u32 = 100; // compile-time constant, must annotate
| Type | Example | Notes |
|---|---|---|
i32, i64, u32, u64 | 42_i32, 1_000_u64 | Signed and unsigned integers |
f32, f64 | 3.14_f64 | f64 is the default |
bool | true, false | 1 byte |
char | 'A', '😀' | 4-byte Unicode scalar |
&str | "hello" | String slice, immutable |
String | String::from("hi") | Heap-allocated, growable |
(T, U) | (1, "two") | Tuple — fixed size, mixed types |
[T; N] | [1, 2, 3] | Fixed-size array on stack |
Vec<T> | vec![1, 2, 3] | Growable list on heap |
Control Flow
03if is an expression — it returns a value. loop is an infinite loop, while is conditional, and for iterates.
// if as expression
let label = if score >= 90 { "A" } else if score >= 80 { "B" } else { "C" };
// for over a range
for i in 0..5 {
println!("{i}"); // 0 1 2 3 4
}
// for over a collection
let names = vec!["Ada", "Linus", "Grace"];
for name in &names {
println!("{name}");
}
// loop with a return value
let answer = loop {
let x = compute();
if x > 100 { break x * 2; }
};
Functions
04Functions use fn, require type annotations for parameters, and the final expression (without semicolon) is the return value.
fn add(a: i32, b: i32) -> i32 {
a + b // no semicolon = return value
}
fn greet(name: &str) { // no -> means returns unit ()
println!("Hello, {name}!");
}
fn main() {
let sum = add(2, 3);
greet("Ada");
}
Ownership
05Rust's killer feature. Three rules:
- Each value has exactly one owner.
- When the owner goes out of scope, the value is dropped.
- You can have many immutable references or one mutable reference — never both at the same time.
let s1 = String::from("hi");
let s2 = s1; // ownership moves to s2
// println!("{s1}"); // ❌ compile error — s1 no longer valid
let n1 = 5;
let n2 = n1; // ✅ Copy types are duplicated (i32 implements Copy)
println!("{n1} {n2}"); // both valid
Borrowing with & and &mut
References let you read or modify a value without taking ownership.
fn length(s: &String) -> usize { // immutable borrow
s.len()
}
fn shout(s: &mut String) { // mutable borrow
s.push_str("!!!");
}
fn main() {
let mut name = String::from("hello");
let len = length(&name); // read-only borrow
shout(&mut name); // mutable borrow
println!("{name} ({len})"); // "hello!!! (5)"
}
Structs
06Structs group related data. Add methods with impl.
struct User {
name: String,
email: String,
active: bool,
}
impl User {
// Associated function (no &self) — acts like a constructor
fn new(name: &str, email: &str) -> Self {
Self {
name: name.to_string(),
email: email.to_string(),
active: true,
}
}
// Method (takes &self)
fn deactivate(&mut self) {
self.active = false;
}
}
let mut u = User::new("Ada", "ada@example.com");
u.deactivate();
Enums & Match
07Enums can hold data. match handles every variant — the compiler enforces it.
enum Shape {
Circle(f64), // radius
Square(f64), // side
Rectangle { w: f64, h: f64 },
}
fn area(s: &Shape) -> f64 {
match s {
Shape::Circle(r) => std::f64::consts::PI * r * r,
Shape::Square(s) => s * s,
Shape::Rectangle { w, h } => w * h,
}
}
let s = Shape::Rectangle { w: 3.0, h: 4.0 };
println!("{}", area(&s)); // 12
Option & Result
08Rust has no null and no exceptions. Option<T> models "maybe a value", Result<T, E> models "success or error".
fn find(name: &str) -> Option<u32> {
if name == "Ada" { Some(36) } else { None }
}
match find("Ada") {
Some(age) => println!("Age: {age}"),
None => println!("Not found"),
}
// Shortcut: unwrap_or, ?, if let
let age = find("Ada").unwrap_or(0);
if let Some(age) = find("Ada") { println!("{age}"); }
use std::num::ParseIntError;
fn parse_age(s: &str) -> Result<u32, ParseIntError> {
let n = s.parse::<u32>()?; // ? returns the error early
Ok(n)
}
fn main() {
match parse_age("42") {
Ok(n) => println!("Got {n}"),
Err(e) => println!("Error: {e}"),
}
}
Next up
Comfortable with ownership and enums? Continue with Rust Mid-Level — traits, generics, lifetimes, iterators, and smart pointers.