Rust: Basics for Experienced Developers in 2026
Learn Rust quickly by leveraging your existing knowledge. Ownership, borrowing, lifetimes, and essential patterns explained for developers coming from C++, Java, or Python.

Rust continues to gain popularity year after year, and for good reason: guaranteed memory safety at compile time, C++-level performance, and a modern ecosystem. For experienced developers coming from C++, Java, or Python, learning Rust can feel disorienting at first, but the fundamental concepts quickly become intuitive once understood.
Rust has been the most loved language on Stack Overflow for 8 consecutive years. Adopted by Microsoft, Google, Amazon, and Meta for critical components, it delivers memory safety without a garbage collector.
Setting Up Your Rust Environment
Before writing code, Rust needs to be installed via rustup, the official version management tool.
# install.sh
# Install Rust via rustup (macOS, Linux)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Verify installation
rustc --version
cargo --versionCargo is Rust's package manager and build tool. It combines the functionality of npm, Maven, and Make into a single coherent tool.
# project-setup.sh
# Create a new project
cargo new my_project
cd my_project
# Generated structure:
# my_project/
# ├── Cargo.toml # Manifest (like package.json)
# └── src/
# └── main.rs # Entry point
# Essential commands
cargo build # Compile the project
cargo run # Compile and run
cargo test # Run tests
cargo check # Check without building (faster)Variables and Default Immutability
Rust reverses the usual convention: variables are immutable by default. This approach forces explicit thinking about mutability and prevents many bugs.
fn main() {
// Immutable by default
let x = 5;
// x = 6; // Compile error!
// Explicitly mutable variable
let mut y = 5;
y = 6; // OK
// Shadowing: redeclaration in the same scope
let x = x + 1; // Creates a new variable x
let x = x * 2; // x is now 12
// Shadowing also allows changing the type
let spaces = " "; // &str
let spaces = spaces.len(); // usize
}Shadowing creates a new variable, unlike mut which modifies the existing value. Shadowing allows transforming a value while keeping a clear name.
Fundamental Data Types
Rust is statically typed with excellent type inference. Here are the essential primitive types to know.
fn main() {
// Signed integers: i8, i16, i32, i64, i128, isize
let age: i32 = 30;
// Unsigned integers: u8, u16, u32, u64, u128, usize
let count: u64 = 1_000_000; // Underscores for readability
// Floats: f32, f64 (default)
let pi: f64 = 3.14159;
// Boolean
let active: bool = true;
// Unicode character (4 bytes)
let emoji: char = '🦀';
// Tuple: fixed collection of different types
let person: (String, i32) = (String::from("Alice"), 28);
let (name, age) = person; // Destructuring
// Array: fixed size, same type
let numbers: [i32; 5] = [1, 2, 3, 4, 5];
let first = numbers[0];
// Slice: view into a portion of data
let slice: &[i32] = &numbers[1..4]; // [2, 3, 4]
}Ownership: The Revolutionary Concept
Ownership is THE innovation of Rust. This system guarantees memory safety without a garbage collector, at the cost of an initial learning curve.
The Three Rules of Ownership
fn main() {
// Rule 1: Each value has a single owner
let s1 = String::from("hello");
// Rule 2: When the owner goes out of scope, the value is freed
{
let s2 = String::from("world");
// s2 is valid here
}
// s2 is dropped, no longer accessible
// Rule 3: Only one ownership at a time (move)
let s3 = s1; // s1 is MOVED to s3
// println!("{}", s1); // Error: s1 is no longer valid!
println!("{}", s3); // OK: s3 is the owner
}Move vs Clone
fn main() {
// Simple types (stack): automatic Copy
let x = 5;
let y = x; // Copy, not move
println!("x = {}, y = {}", x, y); // Both are valid
// Complex types (heap): Move by default
let s1 = String::from("hello");
let s2 = s1; // Move
// s1 is no longer usable
// Explicit clone to duplicate
let s3 = String::from("world");
let s4 = s3.clone(); // Deep copy
println!("s3 = {}, s4 = {}", s3, s4); // Both valid
}Passing a value to a function transfers ownership. The function becomes the owner and the value is no longer accessible after the call, unless returned.
fn main() {
let s = String::from("hello");
takes_ownership(s);
// println!("{}", s); // Error: s has been moved
let x = 5;
makes_copy(x);
println!("{}", x); // OK: i32 implements Copy
// To regain ownership, return the value
let s2 = String::from("hello");
let s3 = takes_and_gives_back(s2);
println!("{}", s3); // OK
}
fn takes_ownership(s: String) {
println!("{}", s);
} // s is dropped here
fn makes_copy(x: i32) {
println!("{}", x);
}
fn takes_and_gives_back(s: String) -> String {
s // Returns ownership
}Borrowing: References Without Transfer
Borrowing allows using a value without taking ownership. This is the most frequently used mechanism in Rust.
fn main() {
let s1 = String::from("hello");
// Immutable reference: read-only
let len = calculate_length(&s1);
println!("Length of '{}': {}", s1, len); // s1 still valid
// Mutable reference: modification allowed
let mut s2 = String::from("hello");
change(&mut s2);
println!("{}", s2); // "hello, world"
}
fn calculate_length(s: &String) -> usize {
s.len()
} // s goes out of scope but doesn't drop (it's a reference)
fn change(s: &mut String) {
s.push_str(", world");
}Borrowing Rules
fn main() {
let mut s = String::from("hello");
// Rule 1: Multiple immutable references simultaneously OK
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
// Rule 2: ONLY ONE mutable reference at a time
let r3 = &mut s;
// let r4 = &mut s; // Error: already borrowed mutably
println!("{}", r3);
// Rule 3: No mutable ref if immutable ref exists
let r5 = &s;
// let r6 = &mut s; // Error: r5 is still active
println!("{}", r5);
// Once r5 is used for the last time, mutable borrow is allowed
let r7 = &mut s; // OK: r5 is no longer used after this
r7.push_str("!");
}Ready to ace your Rust interviews?
Practice with our interactive simulators, flashcards, and technical tests.
Lifetimes: Ensuring Reference Validity
Lifetimes ensure that references remain valid. The compiler often infers them automatically, but sometimes explicit annotation is needed.
// Classic error: reference to freed data
// fn dangling() -> &String {
// let s = String::from("hello");
// &s // Error: s will be dropped, invalid reference!
// }
// Solution: return the owned value
fn no_dangle() -> String {
let s = String::from("hello");
s // Ownership transferred, no problem
}Lifetime Annotations
// The compiler can't figure out which reference will be returned
// fn longest(x: &str, y: &str) -> &str { ... } // Error!
// Explicit annotation: return lives as long as BOTH x AND y
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let string1 = String::from("long string");
let result;
{
let string2 = String::from("xyz");
result = longest(&string1, &string2);
println!("Longest: {}", result); // OK here
}
// println!("{}", result); // Error: string2 dropped
}Rust applies elision rules to avoid annotating simple cases. For beginners, following the compiler's explicit messages is sufficient.
Structs and Implementations
Structs are the building blocks for custom types in Rust.
// Struct definition
#[derive(Debug)] // Enables printing with {:?}
struct User {
username: String,
email: String,
active: bool,
sign_in_count: u64,
}
// Implementation block for methods
impl User {
// Constructor (convention: fn new or descriptive name)
fn new(username: String, email: String) -> Self {
Self {
username,
email,
active: true,
sign_in_count: 1,
}
}
// Method: takes &self (reference to instance)
fn is_active(&self) -> bool {
self.active
}
// Method with mutation: takes &mut self
fn deactivate(&mut self) {
self.active = false;
}
// Method consuming self (rare)
fn into_username(self) -> String {
self.username
}
}
fn main() {
let mut user = User::new(
String::from("alice"),
String::from("alice@example.com"),
);
println!("Active: {}", user.is_active());
user.deactivate();
println!("Active: {}", user.is_active());
// Debug print
println!("{:?}", user);
}Enums and Pattern Matching
Rust enums are far more powerful than in most languages: each variant can contain data.
// Simple enum
enum Direction {
North,
South,
East,
West,
}
// Enum with data (algebraic data type)
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(u8, u8, u8),
}
impl Message {
fn process(&self) {
match self {
Message::Quit => println!("Quitting"),
Message::Move { x, y } => println!("Moving to ({}, {})", x, y),
Message::Write(text) => println!("Writing: {}", text),
Message::ChangeColor(r, g, b) => {
println!("Color: rgb({}, {}, {})", r, g, b)
}
}
}
}
fn main() {
let msg = Message::Move { x: 10, y: 20 };
msg.process();
let msg2 = Message::Write(String::from("Hello Rust!"));
msg2.process();
}Option and Result: Error Handling
use std::fs::File;
use std::io::{self, Read};
fn main() {
// Option<T>: present or absent value (replaces null)
let numbers = vec![1, 2, 3];
let first: Option<&i32> = numbers.first();
match first {
Some(n) => println!("First: {}", n),
None => println!("Empty list"),
}
// Utility methods
let value = first.unwrap_or(&0);
let doubled = first.map(|n| n * 2);
// Result<T, E>: success or error
let result = read_file("config.txt");
match result {
Ok(content) => println!("Content: {}", content),
Err(e) => println!("Error: {}", e),
}
}
fn read_file(path: &str) -> Result<String, io::Error> {
let mut file = File::open(path)?; // ? propagates the error
let mut content = String::new();
file.read_to_string(&mut content)?;
Ok(content)
}The ? operator is syntactic sugar for error propagation. It automatically returns the error if Result is Err, otherwise it unwraps the Ok value.
Traits: Rust-Style Polymorphism
Traits define shared behavior, similar to Java interfaces or Swift protocols.
// Trait definition
trait Summary {
fn summarize(&self) -> String;
// Method with default implementation
fn preview(&self) -> String {
format!("{}...", &self.summarize()[..50.min(self.summarize().len())])
}
}
struct Article {
title: String,
author: String,
content: String,
}
struct Tweet {
username: String,
content: String,
}
// Implementation for Article
impl Summary for Article {
fn summarize(&self) -> String {
format!("{} by {}", self.title, self.author)
}
}
// Implementation for Tweet
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("@{}: {}", self.username, self.content)
}
}
// Function accepting any type implementing Summary
fn notify(item: &impl Summary) {
println!("Breaking news: {}", item.summarize());
}
// Equivalent syntax with trait bound
fn notify_generic<T: Summary>(item: &T) {
println!("Breaking news: {}", item.summarize());
}
fn main() {
let article = Article {
title: String::from("Rust 2026"),
author: String::from("Community"),
content: String::from("..."),
};
let tweet = Tweet {
username: String::from("rustlang"),
content: String::from("Rust is awesome!"),
};
notify(&article);
notify(&tweet);
}Essential Collections
Rust provides powerful collections in the standard library.
use std::collections::HashMap;
fn main() {
// Vec<T>: dynamic array
let mut numbers: Vec<i32> = Vec::new();
numbers.push(1);
numbers.push(2);
numbers.push(3);
// vec! macro for initialization
let nums = vec![1, 2, 3, 4, 5];
// Iteration
for n in &nums {
println!("{}", n);
}
// Functional methods
let doubled: Vec<i32> = nums.iter().map(|x| x * 2).collect();
let sum: i32 = nums.iter().sum();
let evens: Vec<&i32> = nums.iter().filter(|x| *x % 2 == 0).collect();
// String: growable UTF-8 string
let mut s = String::from("Hello");
s.push_str(", World!");
s.push('!');
// Concatenation
let s1 = String::from("Hello, ");
let s2 = String::from("World!");
let s3 = s1 + &s2; // s1 moved, s2 borrowed
// or with format!
let s4 = format!("{}{}", "Hello, ", "World!");
// HashMap<K, V>
let mut scores: HashMap<String, i32> = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Red"), 50);
// Access with get (returns Option)
if let Some(score) = scores.get("Blue") {
println!("Blue: {}", score);
}
// Entry API for conditional insertion
scores.entry(String::from("Yellow")).or_insert(25);
}Idiomatic Error Handling
Proper error handling is essential in Rust. Here are the recommended patterns.
use std::fs::File;
use std::io::{self, Read};
use std::num::ParseIntError;
// Define a custom error type
#[derive(Debug)]
enum AppError {
Io(io::Error),
Parse(ParseIntError),
Custom(String),
}
// Implement From for automatic conversion
impl From<io::Error> for AppError {
fn from(err: io::Error) -> Self {
AppError::Io(err)
}
}
impl From<ParseIntError> for AppError {
fn from(err: ParseIntError) -> Self {
AppError::Parse(err)
}
}
// Function returning Result with custom error
fn read_number_from_file(path: &str) -> Result<i32, AppError> {
let mut file = File::open(path)?; // io::Error -> AppError
let mut content = String::new();
file.read_to_string(&mut content)?;
let number: i32 = content.trim().parse()?; // ParseIntError -> AppError
Ok(number)
}
fn main() {
match read_number_from_file("number.txt") {
Ok(n) => println!("Number: {}", n),
Err(AppError::Io(e)) => println!("IO error: {}", e),
Err(AppError::Parse(e)) => println!("Parse error: {}", e),
Err(AppError::Custom(msg)) => println!("Error: {}", msg),
}
}For real projects, the thiserror crate (for libraries) and anyhow (for applications) greatly simplify error handling.
Testing in Rust
Rust integrates a testing framework directly into the language.
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
pub fn divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
Err(String::from("Division by zero"))
} else {
Ok(a / b)
}
}
// Test module (compiled only for `cargo test`)
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
#[test]
fn test_add_negative() {
assert_eq!(add(-1, 1), 0);
}
#[test]
fn test_divide_success() {
assert_eq!(divide(10, 2), Ok(5));
}
#[test]
fn test_divide_by_zero() {
assert!(divide(10, 0).is_err());
}
#[test]
#[should_panic(expected = "index out of bounds")]
fn test_panic() {
let v = vec![1, 2, 3];
let _ = v[99]; // Panic!
}
}Conclusion
Rust offers a unique paradigm that combines memory safety and performance. The ownership and borrowing concepts feel restrictive at first, but become natural with practice. The Rust compiler is an invaluable ally: its error messages are among the best in the industry.
Getting Started Checklist
- ✅ Install Rust via rustup and master Cargo
- ✅ Understand the difference between immutability and explicit mutability
- ✅ Master the three rules of ownership
- ✅ Practice borrowing with
&and&mutreferences - ✅ Use
OptionandResultinstead of null and exceptions - ✅ Write tests with
#[test]
Start practicing!
Test your knowledge with our interview simulators and technical tests.
The Rust community is welcoming and resources are abundant. The official "The Rust Programming Language" book is available for free online. With these solid foundations, you're ready to explore advanced topics like async/await, macros, and WebAssembly.
Tags
Share
Related articles

Ownership and Borrowing in Rust: Complete Guide
Master Rust's ownership and borrowing system. Understand property rules, references, lifetimes, and advanced memory management patterns.

Rust Interview Questions: Complete Guide 2026
The 25 most common Rust interview questions. Ownership, borrowing, lifetimes, traits, async and concurrency with detailed answers and code examples.

Rust Ownership and Borrowing: The Guide That Demystifies Everything
Master Rust ownership and borrowing with practical examples. Understand move semantics, references, lifetimes, and the borrow checker to write safe, efficient Rust code.