Getting Started with Rust Programming Language

Learn the fundamentals of Rust programming language, including installation, basic syntax, and core concepts.

September 07, 2025
10 min read
#rust #programming #systems #memory-safety #performance
Getting Started with Rust Programming Language - Budibadu

Getting Started with Rust Programming Language

What is Rust?

Rust is a systems programming language that runs blazingly fast, prevents segfaults, and guarantees thread safety. It was created by Mozilla and has gained popularity for its focus on memory safety without garbage collection. Rust combines the performance of C and C++ with the safety and expressiveness of modern languages like Haskell and ML.

Since its first stable release in 2015, Rust has been adopted by major companies including Microsoft, Google, Amazon, Facebook, and Dropbox. It's used in critical systems like operating systems, web browsers, game engines, and blockchain applications.

Why Choose Rust?

Memory Safety: Rust prevents common programming errors like null pointer dereferences, buffer overflows, and use-after-free bugs through its ownership system

Performance: Rust offers C/C++ level performance with modern language features and zero-cost abstractions

Concurrency: Built-in support for safe concurrent programming without data races

Zero-cost Abstractions: High-level features that compile to efficient low-level code

Type Safety: Strong static typing prevents many runtime errors

Ecosystem: Growing package ecosystem with Cargo as the package manager

Cross-platform: Runs on Windows, macOS, Linux, and many embedded systems

Installation and Setup

To install Rust, visit rustup.rs and run the installer:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

After installation, you'll have access to:

rustc - The Rust compiler

cargo - The package manager and build tool

rustup - The toolchain installer and updater

Verifying Installation

After the installer finishes, open a fresh terminal so the new PATH settings take effect. Running rustc and cargo with the --version flag confirms that the compiler and package manager are ready to use. On Windows you can run the same commands in PowerShell or Command Prompt.

If you installed via rustup, you can also run rustup show to inspect the active toolchain, default host, and installed components. When everything is configured correctly you should see the stable toolchain reported without missing components.

rustc --version
cargo --version
rustup show

Your First Rust Program

Rust produces native executables, so you begin by writing a source file and then compiling it with the toolchain. Keeping examples small makes it easier to understand how the language pieces fit together before you start adding dependencies.

The following example prints the classic Hello, world! message and shows how Rust macros such as println! differ from regular function calls. Macros expand at compile time and provide powerful formatting features.

Create a file called main.rs and add the following code:

fn main() {
    println!("Hello, world!");
}

When you compile the file, Rust emits a binary alongside the source. On Linux and macOS the executable is named main, while on Windows it becomes main.exe. Run the program to make sure your toolchain works end to end:

rustc main.rs
./main    # use .\main.exe on Windows

Cargo acts as the standard project orchestrator for Rust: it scaffolds new workspaces, downloads dependencies, builds artifacts, runs tests, and even publishes crates. Adopting it from day one saves you from managing compiler flags and build scripts by hand.

Create a new project with cargo new to generate a Git-ready repository and a starter main.rs file:

cargo new hello_world
cd hello_world
cargo run

The cargo run command compiles the project (rebuilding only what changed) and then executes the resulting binary. As your application grows you can declare dependencies in Cargo.toml, and Cargo will resolve versions, download crates from crates.io, and cache builds automatically.

This creates a proper project structure with:

Cargo.toml - Project configuration

src/main.rs - Source code

target/ - Build artifacts

Basic Syntax and Concepts

Variables and Mutability

In Rust, variables are immutable by default, which prevents accidental modifications:

let x = 5; // immutable
let mut y = 5; // mutable
y = 6; // this is allowed

// Shadowing allows reusing variable names
let x = x + 1; // x is now 6
let x = "hello"; // x is now a string

Data Types

Rust has several primitive data types:

// Integers
let a: i32 = 42;        // 32-bit signed integer
let b: u64 = 100;       // 64-bit unsigned integer
let c = 1_000_000;      // underscores for readability

// Floating-point
let d: f64 = 3.14;      // 64-bit floating point
let e = 2.0;            // f64 by default

// Boolean
let f: bool = true;

// Character (Unicode scalar value)
let g: char = 'R';

// String types
let h: &str = "hello";           // string slice
let i: String = String::from("world"); // owned string

Functions

Functions are declared with fn:

fn add(x: i32, y: i32) -> i32 {
    x + y  // no semicolon = return value
}

fn greet(name: &str) {
    println!("Hello, {}!", name);
}

fn main() {
    let sum = add(5, 3);
    greet("Rust");
    println!("Sum: {}", sum);
}

Control Flow

Rust has familiar control flow constructs:

// if expressions
let number = 6;
if number % 4 == 0 {
    println!("number is divisible by 4");
} else if number % 3 == 0 {
    println!("number is divisible by 3");
} else {
    println!("number is not divisible by 4 or 3");
}

// if as expression
let condition = true;
let number = if condition { 5 } else { 6 };

// loops
loop {
    println!("infinite loop");
    break; // exit loop
}

// while loop
let mut number = 3;
while number != 0 {
    println!("{}!", number);
    number -= 1;
}

// for loop
let a = [10, 20, 30, 40, 50];
for element in a.iter() {
    println!("the value is: {}", element);
}

// range
for number in (1..4).rev() {
    println!("{}!", number);
}

Ownership System

Rust's ownership model enforces compile-time guarantees that eliminate entire classes of memory bugs. Every value has exactly one owner, moves transfer ownership, and the compiler inserts deterministic drop calls when a value goes out of scope. These strict rules allow Rust to run without a garbage collector while still providing memory safety.

The snippet below shows how moving a String invalidates the original binding, and how cloning explicitly duplicates the underlying buffer when you truly need two owned copies:

let s1 = String::from("hello");
let s2 = s1; // s1 is moved to s2
// println!("{}", s1); // This would cause a compile error

// To copy instead of move:
let s1 = String::from("hello");
let s2 = s1.clone(); // expensive but creates a copy
println!("{}", s1); // this works now

Ownership Rules

Keep the three core ownership principles in mind whenever you pass values between functions or threads. If you can reason about who owns a value and when it will be dropped, you can predict whether moves, clones, or borrows are required.

Each value in Rust has a variable that's called its owner

There can only be one owner at a time

When the owner goes out of scope, the value will be dropped

Structs and Enums

Composite types let you model real-world data with named fields and discrete variants. Structs collect related values into a single type, while enums represent a value that can be one of several possibilities. Both forms integrate tightly with pattern matching and methods.

Structs

Structs provide labeled storage that makes code self-documenting. You can choose between classic structs with named fields, tuple structs for lightweight wrappers, and unit-like structs for marker types. Struct update syntax and ownership rules work together to help you reuse existing values safely.

struct User {
    username: String,
    email: String,
    sign_in_count: u64,
    active: bool,
}

// Tuple structs
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

// Unit-like structs
struct AlwaysEqual;

fn main() {
    let user1 = User {
        email: String::from("[email protected]"),
        username: String::from("someusername123"),
        active: true,
        sign_in_count: 1,
    };
    
    let user2 = User {
        email: String::from("[email protected]"),
        username: String::from("anotherusername456"),
        ..user1 // use remaining fields from user1
    };
}

Enums

Enums are ideal when a value must be exactly one of several variants, each potentially carrying different data. They pair naturally with match expressions, helping you model state machines, protocol messages, and optional values in a type-safe way.

enum IpAddr {
    V4(String),
    V6(String),
}

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

impl Message {
    fn call(&self) {
        // method body would be defined here
    }
}

fn main() {
    let home = IpAddr::V4(String::from("127.0.0.1"));
    let m = Message::Write(String::from("hello"));
    m.call();
}

Pattern Matching

Pattern matching is Rust's expressive control-flow superpower. A match expression must be exhaustive, forcing you to handle every possible variant at compile time. The compiler can destructure complex data structures, bind inner values to new variables, and even guard cases with additional conditions.

The example below maps several coin variants to their value in cents and then shows how to work with Option. When you match on optional data, you avoid null checks entirely because the type system encodes the possibility of absence.

Code
enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

// Matching with Option
fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        None => None,
        Some(i) => Some(i + 1),
    }
}

Error Handling

Instead of exceptions, Rust models failure conditions with the Result type. This forces you to decide whether to bubble the error upward, recover with a default value, or terminate the program. The compiler enforces that every Result and Option is handled, preventing silent failures.

The snippet below shows how moving a String invalidates the original binding, and how cloning explicitly duplicates the underlying buffer when you truly need two owned copies:

Code
use std::fs::File;

fn main() {
    let f = File::open("hello.txt");
    
    let f = match f {
        Ok(file) => file,
        Err(error) => {
            panic!("Problem opening the file: {:?}", error);
        },
    };
}

Collections

The standard library offers flexible collection types for storing groups of values. Vectors, strings, and hash maps cover most everyday needs, and they integrate with ownership and borrowing so you can share or mutate data safely.

Vectors

Vectors are growable arrays allocated on the heap. You can push elements to the end, iterate over slices, and borrow elements immutably or mutably depending on your needs. When accessing by index, decide whether you want a panic on out-of-bounds ([]) or an Option result (get).

Code
let v: Vec<i32> = Vec::new();
let v = vec![1, 2, 3];

let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
v.push(8);

let third: &i32 = &v[2];
let third: Option<&i32> = v.get(2);

Strings

Rust distinguishes between string slices (&str) and owned, heap-allocated String values. String grows dynamically and guarantees UTF-8 encoding, but indexing is restricted because characters can span multiple bytes. Methods like push_str, push, and concatenation via + let you build up text efficiently.

Code
let mut s = String::new();
let s = "initial contents".to_string();
let s = String::from("initial contents");

let mut s = String::from("foo");
s.push_str("bar");
s.push('l');

let s1 = String::from("Hello, "");
let s2 = String::from("world!");
let s3 = s1 + &s2; // s1 has been moved here and can no longer be used

Hash Maps

HashMap stores key/value pairs and is perfect for lookups when order does not matter. Keys and values can be any types that implement the appropriate traits, and borrowing rules ensure that mutable references to entries are handled safely.

Code
use std::collections::HashMap;

let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);

let team_name = String::from("Blue");
let score = scores.get(&team_name);

Modules and Packages

Modules let you break large codebases into focused units with explicit visibility rules. Public items can be shared with other modules or crates, while private functions remain encapsulated. Cargo packages, crates, and modules work together to give you fine-grained control over your project's structure.

The example below defines nested modules and demonstrates absolute versus relative paths when calling exported functions:

Code
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
        fn seat_at_table() {}
    }
    
    mod serving {
        fn take_order() {}
        fn serve_order() {}
        fn take_payment() {}
    }
}

pub fn eat_at_restaurant() {
    // Absolute path
    crate::front_of_house::hosting::add_to_waitlist();
    
    // Relative path
    front_of_house::hosting::add_to_waitlist();
}

Conclusion

Rust is an excellent choice for systems programming, web development, game development, and anywhere you need both performance and safety. Its learning curve can be steep initially, but the benefits of memory safety, performance, and expressiveness make it worth the investment. Start with the basics, practice regularly, and gradually explore more advanced features.

The Rust community is welcoming and helpful, with excellent documentation, tutorials, and resources available. The language continues to evolve rapidly, with new features and improvements in each release.

Share:
Found this helpful?