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 Guide for Experienced Developers

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.

Why Rust in 2026?

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.

bash
# install.sh
# Install Rust via rustup (macOS, Linux)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Verify installation
rustc --version
cargo --version

Cargo is Rust's package manager and build tool. It combines the functionality of npm, Maven, and Make into a single coherent tool.

bash
# 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.

variables.rsrust
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 vs Mutability

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.

types.rsrust
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

ownership.rsrust
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

move_clone.rsrust
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
}
Move and Functions

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.

ownership_functions.rsrust
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.

borrowing.rsrust
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

borrowing_rules.rsrust
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.

lifetimes_basic.rsrust
// 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

lifetimes_annotation.rsrust
// 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
}
Lifetime Elision

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.

structs.rsrust
// 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.

enums.rsrust
// 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

option_result.rsrust
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

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.

traits.rsrust
// 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.

collections.rsrust
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.

error_handling.rsrust
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),
    }
}
Recommended Crates

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.

lib.rsrust
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 &mut references
  • ✅ Use Option and Result instead 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

#rust
#systems programming
#ownership
#memory safety
#performance

Share

Related articles