Rust Basics
10 examples to get you started with Rust fundamentals - 7 basic and 3 intermediate.
Prerequisites
- Rust 1.97.0 with edition 2024 (
rustup default stable) cargo --versionprints 1.97 or newer
Basic Examples
1. Hello with main
Every Rust program starts at main.
fn main() {
println!("Hello, Rust!");
}cargo new helloscaffolds a binary cratecargo runcompiles and executesprintln!is a macro, not a function- Semicolons end statements; expressions without
;are returned values
Related: Formatting & Printing -
Displayand format specifiers
2. Immutable and Mutable Bindings
Variables are immutable unless you opt in with mut.
fn main() {
let x = 5;
// x = 6; // compile error
let mut count = 0;
count += 1;
println!("{count}");
}letcreates a binding; immutability is the defaultmutallows reassignment of the binding- Immutability does not mean the value behind a reference cannot change
- Prefer
letwithoutmutuntil mutation is required
Related: Variables, Mutability & Shadowing - shadowing and constants
3. Scalar Types
Rust has fixed-size integers, floats, bool, and char (Unicode scalar).
fn main() {
let age: u32 = 30;
let ratio: f64 = 0.75;
let active: bool = true;
let letter: char = 'R';
println!("{age} {ratio} {active} {letter}");
}- Integer types:
i8..i128,u8..u128,isize,usize - Type inference fills in types when unambiguous
charis 4 bytes (Unicode), not 1 byte like C- Literals support suffixes:
42u64,3.14f32
Related: Scalar & Compound Types - tuples and arrays
4. Functions and Return Values
The last expression in a block is the return value.
fn add(a: i32, b: i32) -> i32 {
a + b
}
fn main() {
let sum = add(2, 3);
println!("{sum}");
}- Parameter types and return type are required (except for
main) returnis explicit but rarely needed- A trailing
;turns an expression into a statement (discards value) - Functions are first-class names, not methods on objects
Related: Functions & Expressions - expressions vs statements
5. if as an Expression
Branches can produce values when both arms return the same type.
fn main() {
let n = 7;
let label = if n % 2 == 0 { "even" } else { "odd" };
println!("{n} is {label}");
}if/elsemust return compatible types in all arms- No implicit truthiness: conditions must be
bool - Use
if letfor single-pattern matches (covered later) - Prefer expressions over temporary
let+ mutation
Related: Control Flow - loops and
match
6. Ownership Preview
Each value has one owner; assigning moves ownership for non-Copy types.
fn main() {
let s1 = String::from("hello");
let s2 = s1; // move - s1 is no longer valid
// println!("{s1}"); // error: value moved
println!("{s2}");
}Stringowns heap data;&strborrows a string slice- Moves prevent double-free without a garbage collector
- Stack integers (
i32) implementCopyand duplicate instead of move - Borrowing with
&shares read access without transferring ownership
Related: Ownership Preview - moves and borrows in depth
7. References and Borrowing
Borrow with & for read-only access, &mut for exclusive mutation.
fn len(s: &String) -> usize {
s.len()
}
fn main() {
let mut text = String::from("rust");
println!("{}", len(&text));
text.push_str("ace");
println!("{text}");
}- One
&mutor many&to the same data at a time - References must not outlive the data they point to
- Slices (
&[T],&str) are fat pointers to contiguous sequences - The borrow checker enforces these rules at compile time
Related: References & Slices - slice syntax and bounds
Intermediate Examples
8. Pattern Matching with match
match is exhaustive: every variant must be handled.
enum Status {
Ok,
Err(u16),
}
fn describe(s: Status) -> &'static str {
match s {
Status::Ok => "success",
Status::Err(code) => {
println!("error code: {code}");
"failure"
}
}
}
fn main() {
println!("{}", describe(Status::Ok));
}_is a wildcard arm for catch-all patterns- Bindings (
code) extract inner values from enum variants matchonOption/Resultis the idiomatic error path- Compiler errors if you miss a variant
Related: Control Flow -
if letand loops
9. Result for Recoverable Errors
Functions that can fail return Result<T, E>.
use std::fs;
fn read_config(path: &str) -> Result<String, std::io::Error> {
fs::read_to_string(path)
}
fn main() {
match read_config("config.toml") {
Ok(body) => println!("{body}"),
Err(e) => eprintln!("failed: {e}"),
}
}Ok(T)carries success;Err(E)carries failure?propagates errors in functions returningResult- Panics (
panic!,unwrap) are for unrecoverable bugs - Libraries use typed errors; binaries often use
anyhow
Related: Ownership Preview - error types are enums too
10. A Minimal Unit Test
Tests live in the same file or in tests/ integration directories.
pub fn double(n: i32) -> i32 {
n * 2
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn doubles_values() {
assert_eq!(double(3), 6);
}
}#[test]marks a function forcargo testassert_eq!compares debug output on failure- Unit tests share the module's private API
- Integration tests in
tests/*.rssee only the public API
Related: Rust Fundamentals Best Practices - habits before advanced topics
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.