Rust is one of the most loved programming languages — and for good reason. It gives you systems-level control with memory safety guarantees, all enforced at compile time. No garbage collector, no runtime overhead, no segfaults.
This post walks through 6 core concepts you need to understand Rust properly.
Table of Contents
- Ownership & Move Semantics
- Borrowing & References
- Lifetimes
- Structs & Traits
- Error Handling with Result & Option
- Pattern Matching
1. Ownership & Move Semantics
Every value in Rust has a single owner. When the owner goes out of scope, the value is automatically dropped — no garbage collector needed. When you assign a value to another variable, ownership moves.
🦀 Rule: Each value has exactly one owner. When the owner is gone, the memory is freed. No dangling pointers, no double-frees.
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 is MOVED into s2
// println!("{}", s1); ← compile error: value moved!
println!("{}", s2); // ✓ s2 is the owner now
}
// To copy instead of move, use .clone():
let s3 = s2.clone();
println!("{} {}", s2, s3); // both are valid
Ownership flow:
s1 owns "hello" → move → s2 owns "hello" → drop → s1 is invalid
2. Borrowing & References
Instead of moving, you can borrow a value using a reference (&). Immutable borrows allow many readers simultaneously. Mutable borrows (&mut) are exclusive — you can't have other borrows active at the same time.
The rules:
- You can have many immutable references (
&T) at once - OR you can have one mutable reference (
&mut T) — but not both
fn print_len(s: &String) { // borrows, does not own
println!("length: {}", s.len());
}
fn append(s: &mut String) { // mutable borrow
s.push_str(" world");
}
fn main() {
let mut s = String::from("hello");
print_len(&s); // s is still valid after this
append(&mut s);
println!("{}", s); // "hello world"
}
3. Lifetimes
Lifetimes are Rust's way of ensuring references never outlive the data they point to. In most cases the compiler infers them, but sometimes you need to annotate explicitly with 'a syntax.
💡 Lifetimes don't change how long data lives — they describe relationships between references so the compiler can verify safety at compile time.
// 'a means: output lives at least as long as both inputs
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
fn main() {
let s1 = String::from("long string");
let result;
{
let s2 = String::from("xyz");
result = longest(s1.as_str(), s2.as_str());
println!("{}", result); // ✓ both alive here
}
// println!("{}", result); ← error: s2 dropped!
}
4. Structs & Traits
Rust uses struct to define custom data types and trait to define shared behavior — similar to interfaces in other languages. A type can implement many traits. The compiler enforces trait contracts statically, with zero runtime overhead.
trait Describe {
fn describe(&self) -> String;
}
struct Circle { radius: f64 }
struct Rectangle { width: f64, height: f64 }
impl Describe for Circle {
fn describe(&self) -> String {
format!("Circle with radius {}", self.radius)
}
}
impl Describe for Rectangle {
fn describe(&self) -> String {
format!("{}×{} rectangle", self.width, self.height)
}
}
fn print_shape(shape: &impl Describe) {
println!("{}", shape.describe());
}
fn main() {
print_shape(&Circle { radius: 3.0 });
print_shape(&Rectangle { width: 4.0, height: 5.0 });
}
Output:
Circle with radius 3
4×5 rectangle
5. Error Handling with Result & Option
Rust has no exceptions. Instead, functions that can fail return Result<T, E> and functions that may return nothing return Option<T>. The ? operator makes propagating errors ergonomic.
use std::num::ParseIntError;
fn double_parse(s: &str) -> Result<i32, ParseIntError> {
let n = s.parse::<i32>()?; // ? returns early on error
Ok(n * 2)
}
fn main() {
match double_parse("21") {
Ok(val) => println!("Result: {}", val), // 42
Err(e) => println!("Error: {}", e),
}
// Option: Some or None
let items = vec![1, 2, 3];
if let Some(first) = items.first() {
println!("first: {}", first);
}
}
Comparison with other languages:
| Language | Error model |
|---|---|
| Rust |
Result<T, E> — explicit, compiler-enforced |
| Go |
(value, error) — explicit but not enforced |
| Java/Python | Exceptions — implicit, can be ignored |
| C | Return codes — easy to ignore |
6. Pattern Matching
The match expression is Rust's powerhouse. It's exhaustive — the compiler forces you to handle every case. You can match on values, types, struct fields, ranges, and more, all with destructuring.
enum Command {
Quit,
Move { x: i32, y: i32 },
Print(String),
}
fn handle(cmd: Command) {
match cmd {
Command::Quit => println!("Quit!"),
Command::Move { x, y } => println!("Move to ({x},{y})"),
Command::Print(msg) => println!("Msg: {msg}"),
}
}
// Guards and ranges
let score = 85;
let grade = match score {
90..=100 => "A",
80..=89 => "B",
70..=79 => "C",
_ => "F",
};
println!("Grade: {grade}");
Wrapping Up
These six concepts form the backbone of Rust's unique approach to safe, fast systems programming:
| Concept | What it gives you |
|---|---|
| Ownership | Memory safety without a GC |
| Borrowing | Share data safely, concurrently |
| Lifetimes | Compile-time dangling pointer prevention |
| Traits | Zero-cost polymorphism |
| Result/Option | Explicit, unignorable error handling |
| Pattern matching | Exhaustive, expressive control flow |
The best way to learn Rust is to fight the borrow checker — every error it throws is teaching you something real about memory. Start with The Rust Book (it's free!) and Rustlings for hands-on exercises.
If this helped you, drop a ❤️ and share with someone learning Rust!
Top comments (0)