Backend

Rust Programming Language: Getting Started

Mayur Dabhi
Mayur Dabhi
May 31, 2026
15 min read

Rust has been voted the most loved programming language in Stack Overflow's Developer Survey for nine consecutive years — and for good reason. It gives you the raw performance of C and C++ while eliminating entire categories of bugs at compile time, with no garbage collector to slow you down. Whether you're building web services, command-line tools, embedded systems, or WebAssembly modules, Rust delivers speed, safety, and reliability in a single package.

This guide takes you from zero to productive in Rust. You'll install the toolchain, understand the ownership model that makes Rust unique, write real code with structs and enums, and build a small practical program — all while learning the reasoning behind Rust's design decisions.

Why Rust Is Different

Most languages choose between safety and performance. Go uses a garbage collector for safety but adds latency. C gives you full control but lets you shoot yourself in the foot. Rust's ownership system enforces memory safety rules at compile time, so there's no runtime overhead and no undefined behavior — the compiler catches the errors before your code ever runs.

Rust vs the Competition

Before diving into code, it helps to understand where Rust sits relative to other popular languages. The trade-offs are real, and choosing Rust deliberately makes learning it much easier.

Feature Rust Go C++ Python
Memory management Ownership (compile-time) Garbage collector Manual / RAII Garbage collector
Runtime speed Native (C-level) Fast (GC pauses) Native (C-level) Slow (interpreted)
Memory safety Guaranteed Guaranteed Programmer's responsibility Guaranteed
Concurrency safety Data-race free (compile-time) Channels + go routines Programmer's responsibility GIL-limited
Learning curve Steep Gentle Very steep Easy
Use cases Systems, web, WASM, CLI Services, CLI, DevOps Systems, games, OS Scripting, data, ML

Rust's sweet spot is workloads where you need predictable latency and zero overhead — game engines, network services, OS kernels, compilers, and browser engines. Mozilla built Firefox's rendering engine in Rust; Amazon runs large parts of AWS infrastructure on it; and the Linux kernel has accepted Rust as its second official language.

Installation and Your First Program

Rust is installed through rustup, the official toolchain manager. It handles the compiler (rustc), the package manager and build tool (cargo), and standard library updates in one command.

1

Install rustup

On macOS and Linux, run the official installer script. On Windows, download and run rustup-init.exe from rustup.rs.

Terminal (macOS / Linux)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Restart your shell, then verify
rustc --version    # rustc 1.78.0 (or newer)
cargo --version    # cargo 1.78.0
2

Create a new project with Cargo

Cargo is Rust's all-in-one build tool and package manager. It creates projects, manages dependencies, builds, tests, and runs your code.

Terminal
# Create a new binary project
cargo new hello-rust
cd hello-rust

# Project structure:
# hello-rust/
# ├── Cargo.toml    ← project metadata and dependencies
# └── src/
#     └── main.rs   ← entry point

# Build and run
cargo run
3

Your first Rust program

Open src/main.rs. The generated file already contains a Hello World. Let's extend it to explore basic syntax.

src/main.rs
fn main() {
    // Variables are immutable by default
    let name = "Rust";
    let version: u32 = 1;

    // Use `mut` to make a variable mutable
    let mut count = 0;
    count += 1;

    println!("Hello from {} {}!", name, version);
    println!("Count: {}", count);

    // Shadowing: re-bind a name to a new value
    let spaces = "   ";
    let spaces = spaces.len(); // now an integer
    println!("Spaces: {}", spaces);

    // Basic arithmetic
    let sum = 5 + 10;
    let product = 4 * 30;
    let remainder = 43 % 5;
    println!("Sum={} Product={} Remainder={}", sum, product, remainder);
}
4

Add a dependency

Edit Cargo.toml to add packages from crates.io, Rust's package registry. Run cargo build to download and compile them.

Cargo.toml
[package]
name = "hello-rust"
version = "0.1.0"
edition = "2021"

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
rand = "0.8"

Ownership: The Heart of Rust

Ownership is Rust's most distinctive feature — the mechanism that makes memory safety possible without a garbage collector. Every other language either tracks heap memory at runtime (GC) or makes the programmer track it manually. Rust does it at compile time with three simple rules:

  1. Each value in Rust has a single owner.
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, the value is dropped (freed).
Stack s1 : String ptr | len=5 | cap=5 s1 (invalid) moved — cannot use s2 : String ptr | len=5 | cap=5 Heap "hello" h e l l o One allocation, one owner owns move Borrowing &s2 (immutable) read-only reference &mut s2 exclusive mutable ref Rust Ownership and Borrowing Model

When s1 is moved into s2, s1 is no longer valid — the compiler enforces this. Borrowing lets you use values without taking ownership.

Ownership in Practice
fn main() {
    // --- MOVE semantics ---
    let s1 = String::from("hello");
    let s2 = s1;           // s1 is *moved* into s2
    // println!("{}", s1); // ERROR: s1 is no longer valid!
    println!("{}", s2);    // OK

    // --- CLONE: explicit deep copy ---
    let s3 = s2.clone();   // expensive, but both s2 and s3 are valid
    println!("s2={} s3={}", s2, s3);

    // --- Copy types (stack-only values like integers) ---
    let x = 5;
    let y = x; // integers implement Copy, so x is still valid
    println!("x={} y={}", x, y);

    // --- BORROWING: pass a reference instead of moving ---
    let s4 = String::from("world");
    let len = calculate_length(&s4); // pass a reference
    println!("{} has {} chars", s4, len); // s4 still valid

    // --- MUTABLE borrowing ---
    let mut s5 = String::from("hello");
    change(&mut s5);
    println!("{}", s5); // "hello world"
}

fn calculate_length(s: &String) -> usize {
    s.len()  // s is a reference; no ownership is taken
}

fn change(s: &mut String) {
    s.push_str(" world");
}

The Borrowing Rules

Rust enforces two borrowing rules at compile time that eliminate data races entirely:

The Learning Curve Is Real

Fighting the borrow checker is the number-one frustration for Rust beginners. When the compiler rejects your code, it almost always means there's a genuine bug — a potential data race, a use-after-free, or a dangling reference. Trust the compiler. When it tells you something is wrong, the error message usually explains exactly what to do instead.

Structs, Enums, and Pattern Matching

Rust uses structs to group related data and enums to represent values that can be one of several variants. Together with pattern matching, these three features give you expressive, type-safe data modeling without classes or inheritance.

Structs and Methods

Structs and impl blocks
// Define a struct
#[derive(Debug)]
struct Rectangle {
    width: f64,
    height: f64,
}

// Implement methods on the struct
impl Rectangle {
    // Associated function (like a static method / constructor)
    fn new(width: f64, height: f64) -> Rectangle {
        Rectangle { width, height }
    }

    // Method: takes &self (immutable reference to self)
    fn area(&self) -> f64 {
        self.width * self.height
    }

    fn perimeter(&self) -> f64 {
        2.0 * (self.width + self.height)
    }

    fn is_square(&self) -> bool {
        self.width == self.height
    }

    // Method that takes another Rectangle
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

fn main() {
    let rect1 = Rectangle::new(30.0, 50.0);
    let rect2 = Rectangle { width: 10.0, height: 40.0 };

    println!("rect1 = {:?}", rect1);
    println!("Area: {}", rect1.area());
    println!("Perimeter: {}", rect1.perimeter());
    println!("Is square: {}", rect1.is_square());
    println!("rect1 can hold rect2: {}", rect1.can_hold(&rect2));
}

Enums with Data

Rust enums are far more powerful than those in most languages — each variant can carry data of different types. This is the foundation of Rust's error-handling and option types.

Enums and Pattern Matching
#[derive(Debug)]
enum Shape {
    Circle(f64),                       // one f64 (radius)
    Rectangle(f64, f64),               // width, height
    Triangle { base: f64, height: f64 } // named fields
}

impl Shape {
    fn area(&self) -> f64 {
        match self {
            Shape::Circle(r) => std::f64::consts::PI * r * r,
            Shape::Rectangle(w, h) => w * h,
            Shape::Triangle { base, height } => 0.5 * base * height,
        }
    }

    fn name(&self) -> &str {
        match self {
            Shape::Circle(_)      => "Circle",
            Shape::Rectangle(..)  => "Rectangle",
            Shape::Triangle { .. } => "Triangle",
        }
    }
}

fn describe_number(n: i32) -> &'static str {
    match n {
        0         => "zero",
        1..=9     => "single digit",
        10..=99   => "double digit",
        100..=999 => "triple digit",
        _         => "large number",  // _ is the catch-all
    }
}

fn main() {
    let shapes = vec![
        Shape::Circle(5.0),
        Shape::Rectangle(4.0, 6.0),
        Shape::Triangle { base: 3.0, height: 8.0 },
    ];

    for shape in &shapes {
        println!("{}: area = {:.2}", shape.name(), shape.area());
    }

    println!("{}", describe_number(42));  // "double digit"
}

if let — Concise Pattern Matching

if let syntax
let config_max = Some(3u8);

// Verbose match:
match config_max {
    Some(max) => println!("Max is {}", max),
    _ => (),
}

// Concise if let:
if let Some(max) = config_max {
    println!("Max is {}", max);
}

// with else:
if let Some(max) = config_max {
    println!("Max is {}", max);
} else {
    println!("No max configured");
}

Error Handling with Result and Option

Rust has no exceptions. Instead, functions that can fail return a Result<T, E>, and functions that might return nothing return an Option<T>. This forces callers to handle failure explicitly — a design that eliminates an entire class of runtime crashes.

Option<T> replaces null. It is either Some(value) or None:

fn find_first_even(numbers: &[i32]) -> Option<i32> {
    for &n in numbers {
        if n % 2 == 0 {
            return Some(n);
        }
    }
    None
}

fn main() {
    let nums = vec![1, 3, 5, 8, 11];

    match find_first_even(&nums) {
        Some(n) => println!("First even: {}", n),
        None    => println!("No even numbers"),
    }

    // Convenience methods on Option
    let result = find_first_even(&nums);
    println!("{}", result.unwrap_or(0));          // 8
    println!("{}", result.is_some());             // true
    println!("{}", result.map(|n| n * 2).unwrap()); // 16

    // unwrap() panics if None — only use when you're certain
    let x: Option<i32> = Some(42);
    println!("{}", x.unwrap());
}

Result<T, E> represents either success (Ok(T)) or failure (Err(E)):

use std::num::ParseIntError;

fn parse_and_double(s: &str) -> Result<i32, ParseIntError> {
    let n = s.trim().parse::<i32>()?;  // ? propagates the error
    Ok(n * 2)
}

fn main() {
    match parse_and_double("21") {
        Ok(n)  => println!("Result: {}", n),   // Result: 42
        Err(e) => println!("Error: {}", e),
    }

    match parse_and_double("abc") {
        Ok(n)  => println!("Result: {}", n),
        Err(e) => println!("Error: {}", e),  // invalid digit found in string
    }

    // Convenience methods
    let ok: Result<i32, &str> = Ok(10);
    println!("{}", ok.unwrap_or(0));         // 10
    println!("{}", ok.is_ok());              // true
    println!("{}", ok.map(|n| n + 1).unwrap()); // 11
}

The ? operator is syntactic sugar for propagating errors up the call stack:

use std::fs;
use std::io;

// Without ? — verbose
fn read_username_verbose(path: &str) -> Result<String, io::Error> {
    let result = fs::read_to_string(path);
    match result {
        Ok(s)  => Ok(s.trim().to_string()),
        Err(e) => Err(e),
    }
}

// With ? — clean and idiomatic
fn read_username(path: &str) -> Result<String, io::Error> {
    let username = fs::read_to_string(path)?.trim().to_string();
    Ok(username)
}

// Chain multiple fallible operations
fn read_and_process(path: &str) -> Result<Vec<String>, io::Error> {
    let content = fs::read_to_string(path)?;
    let lines: Vec<String> = content
        .lines()
        .map(|l| l.to_uppercase())
        .collect();
    Ok(lines)
}

fn main() {
    match read_username("user.txt") {
        Ok(name) => println!("Hello, {}!", name),
        Err(e)   => eprintln!("Could not read username: {}", e),
    }
}

Use panic! for unrecoverable programmer errors; use Result for expected failure scenarios:

// panic! — for bugs that should never happen in correct code
fn get_first(v: &[i32]) -> i32 {
    if v.is_empty() {
        panic!("get_first called with empty slice — this is a bug");
    }
    v[0]
}

// Result — for expected failures (file not found, bad input, etc.)
fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        return Err(String::from("Division by zero"));
    }
    Ok(a / b)
}

// Guidelines:
// - panic! when an invariant is broken (bad index, logic error)
// - Result when failure is expected and callers should handle it
// - unwrap() only in prototypes or tests where panic is acceptable

fn main() {
    // Good: handle the Result
    match divide(10.0, 3.0) {
        Ok(result) => println!("{:.2}", result),
        Err(e)     => println!("Error: {}", e),
    }

    // Lazy: only acceptable in short scripts / tests
    let result = divide(10.0, 2.0).unwrap();
    println!("{}", result);
}

Concurrency: Fearless by Design

Rust's ownership system doesn't just prevent memory bugs — it prevents data races at compile time. If two threads could access the same data unsafely, the code simply won't compile. This is what the Rust team calls "fearless concurrency."

Main Thread spawn spawn Thread 1 Arc<Mutex<T>> Thread 2 Arc<Mutex<T>> Shared Data Mutex protected Channel (mpsc) tx.send(msg) rx.recv() Rust Thread Safety: Ownership Prevents Data Races

Rust enforces thread safety at compile time. Arc (atomic reference count) + Mutex provides shared state; channels provide message passing.

Threads and Channels
use std::thread;
use std::sync::{Arc, Mutex};
use std::sync::mpsc;  // multiple producer, single consumer

fn main() {
    // --- Threads with shared state via Arc<Mutex<T>> ---
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();
            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }
    println!("Counter: {}", *counter.lock().unwrap()); // 10

    // --- Message passing with channels ---
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        let messages = vec!["hello", "from", "thread"];
        for msg in messages {
            tx.send(msg).unwrap();
            thread::sleep(std::time::Duration::from_millis(100));
        }
    });

    for received in rx {
        println!("Got: {}", received);
    }
}
Send and Sync Traits

Rust's thread safety guarantees come from two marker traits: Send (a type can be transferred to another thread) and Sync (a type can be shared between threads). The compiler automatically checks these — you can't accidentally pass a non-thread-safe type across thread boundaries.

Real-World Example: A CLI Task Manager

Let's put everything together and build a small command-line task manager. It demonstrates structs, enums, pattern matching, error handling, and file I/O — the patterns you'll use in every real Rust project.

src/main.rs — CLI Task Manager
use std::fs;
use std::io::{self, BufRead, Write};

#[derive(Debug)]
struct Task {
    id: usize,
    title: String,
    done: bool,
}

impl Task {
    fn new(id: usize, title: &str) -> Self {
        Task {
            id,
            title: title.to_string(),
            done: false,
        }
    }

    fn to_line(&self) -> String {
        format!("{}|{}|{}", self.id, self.title, self.done)
    }
}

fn load_tasks(path: &str) -> Vec<Task> {
    let Ok(content) = fs::read_to_string(path) else {
        return vec![];
    };
    content
        .lines()
        .filter_map(|line| {
            let parts: Vec<&str> = line.splitn(3, '|').collect();
            if parts.len() == 3 {
                Some(Task {
                    id: parts[0].parse().ok()?,
                    title: parts[1].to_string(),
                    done: parts[2] == "true",
                })
            } else {
                None
            }
        })
        .collect()
}

fn save_tasks(path: &str, tasks: &[Task]) -> io::Result<()> {
    let mut file = fs::File::create(path)?;
    for task in tasks {
        writeln!(file, "{}", task.to_line())?;
    }
    Ok(())
}

fn main() {
    let path = "tasks.txt";
    let mut tasks = load_tasks(path);

    let args: Vec<String> = std::env::args().collect();
    match args.get(1).map(String::as_str) {
        Some("add") => {
            let title = args[2..].join(" ");
            let id = tasks.len() + 1;
            tasks.push(Task::new(id, &title));
            save_tasks(path, &tasks).unwrap();
            println!("Added task #{}: {}", id, title);
        }
        Some("done") => {
            if let Some(id) = args.get(2).and_then(|s| s.parse::<usize>().ok()) {
                if let Some(task) = tasks.iter_mut().find(|t| t.id == id) {
                    task.done = true;
                    save_tasks(path, &tasks).unwrap();
                    println!("Task #{} marked as done", id);
                } else {
                    eprintln!("Task {} not found", id);
                }
            }
        }
        Some("list") | None => {
            for task in &tasks {
                let status = if task.done { "✓" } else { "○" };
                println!("[{}] #{} {}", status, task.id, task.title);
            }
            if tasks.is_empty() {
                println!("No tasks. Add one: cargo run -- add Buy groceries");
            }
        }
        Some(cmd) => eprintln!("Unknown command: {}", cmd),
    }
}

Essential Cargo Commands Cheatsheet

CommandDescription
cargo new myappCreate a new binary project
cargo new mylib --libCreate a library crate
cargo buildCompile in debug mode
cargo build --releaseOptimised production build
cargo runBuild and run
cargo run -- add Buy milkPass arguments to your program
cargo testRun all tests
cargo checkType-check without producing a binary (fast)
cargo clippyRun the linter for idiomatic suggestions
cargo fmtFormat code with rustfmt
cargo doc --openGenerate and open documentation
cargo add serdeAdd a dependency (cargo-edit)
cargo updateUpdate all dependencies

Where to Go From Here

You've now seen the concepts that define Rust: ownership, borrowing, enums with pattern matching, and error handling with Result. These ideas take practice to internalize, but once they click, writing Rust feels remarkably productive — the compiler becomes your pair programmer, catching bugs before they reach production.

Recommended Learning Path

  • The Rust Book — the official guide at doc.rust-lang.org/book, available free online. Chapters 10 (generics, traits, lifetimes) and 13 (iterators, closures) are especially important.
  • Rustlings — small exercises that teach syntax and ownership hands-on. Run cargo install rustlings to start.
  • Iterators and closures — Rust's functional-style collection processing is powerful and idiomatic. Learn map, filter, fold, and collect.
  • Traits — Rust's answer to interfaces. They enable polymorphism without inheritance and are the backbone of every major Rust library.
  • async/await — Rust's asynchronous programming model with tokio or async-std is the foundation for high-performance web services.
  • Axum or Actix-Web — build production-ready HTTP APIs in Rust. Both are battle-tested and extremely fast.
"Rust's goal is not to be the easiest language to learn. It's to be the language where the difficulty you encounter is the actual difficulty of the problem, not an artifact of the language itself."
— Rust community

The investment in learning Rust's ownership model pays dividends for years: programs that don't crash due to null pointer dereferences, no memory leaks, no data races, and performance that rivals hand-optimised C. Start with small projects, lean on the compiler's error messages, and don't hesitate to read the generated documentation with cargo doc --open. The community is welcoming, the tooling is exceptional, and the language keeps getting better.

Rust Systems Programming Performance Memory Safety Ownership Backend
Mayur Dabhi

Mayur Dabhi

Full Stack Developer with 5+ years of experience building scalable web applications with Laravel, React, and Node.js.