My journey on learning Rust from scratch
July 23, 2025

| 14 min Read

My journey on learning Rust from scratch


A month ago I started my journey on learning rust, the robust language that is best fitted for backend systems providing memory safety and speed. Its like using classic languages like C or C++ but without worrying about allocating or de-allocating memory. 

Having already experience in programming with c++ in my college days getting the basics was not that hard , Rust also starts the code execution form the main function , so it is like the entry point for your code.


Getting Started: Familiar Syntax


Just like C++, Rust’s entry point is the main function:

fn main() {
// Your code goes here
}

The basic data types felt familiar:

fn main() {
let integer_ = 23;
let float_64 = 12.2;
let char = 'c';
let boolean = true;
let string_ = String::from("this is a string");
}

You can also explicitly set types to control memory usage:

fn main() {
let age: i32 = 23;
let name: String = String::from("John");
println!("age:{}, name:{}", age, name);
}


Structs: Structuring Data


As I dived deeper, I discovered structs, which reminded me of C++ “structs” or “classes” but designed in a uniquely Rust way. They allow you to group related data:

struct Person {
name: String,
age: i32,
}
fn main() {
let new_person = Person {
name: String::from("John"),
age: 32,
};
println!("Age:{}, name:{}", new_person.age, new_person.name);
}

This structure not only makes the data readable but also paves the way for associated methods and behaviors.


Implementation Blocks: Adding Functionality


Next, I explored implementation blocks, or impl. These are how you attach functions or methods to your structs, much like how you add member functions to a C++ class.

struct Person {
name: String,
age: i32,
}
impl Person {
// Associated function (like a constructor)
fn new(name: String, age: i32) -> Person {
Person { name, age }
}
// Method (operates on self)
fn greet(&self) {
println!("Hello, my name is {} and I am {} years old.", self.name, self.age);
}
}
fn main() {
let person = Person::new(String::from("Alice"), 28);
person.greet();
}

With this, not only can you create and organize data, but you also encapsulate behavior with your types — something I found powerful and elegant.


Enums: Representing Variants


One Rust feature that stood out is enums — they can store different types and amounts of data under a single type, far beyond what C++ enum can do.

enum Status {
Success,
Error(String),
}

fn print_status(status: Status) {
match status {
Status::Success => println!("Operation was successful!"),
Status::Error(msg) => println!("Error: {}", msg),
}
}
fn main() {
let res = Status::Error(String::from("File not found"));
print_status(res);
}

Enums paired with powerful pattern matching allow for expressive and error-resistant code.


Traits: Polymorphism in Rust


Finally, I learned about traits, Rust’s version of interfaces or abstract classes. Traits define shared functionality, letting you write generic and reusable code:

trait Greet {
fn greet(&self);
}
struct Person {
name: String,
}
impl Greet for Person {
fn greet(&self) {
println!("Hi, I'm {}.", self.name);
}
}
fn main() {
let john = Person { name: String::from("John") };
john.greet();
}

With traits, you can enforce that different types implement certain behavior, combining safety and flexibility.


Diving Deeper: Ownership and Lifetimes in Rust


Learning Rust means embracing the language’s unique memory management model, which is centered on ownership and lifetimes. These are the features that set Rust apart from many other languages, especially for ensuring safety without a garbage collector.


Ownership: Ensuring Memory Safety


At the heart of Rust is the ownership system. Every value has a single owner (usually a variable), and the owner is responsible for cleaning up the value when it’s no longer needed. Here are the key rules:

  • Each value has a single owner.
  • When the owner goes out of scope, the value is dropped (freed from memory).
  • Assigning or passing ownership to another variable or function moves the value, invalidating the original reference.


A classic example:


fn main() {
let s = String::from("hello");
takes_ownership(s);
// s is no longer valid here
let x = 5;
makes_copy(x);
// x is still valid here because integers implement the Copy trait
}
fn takes_ownership(str: String) {
println!("{}", str);
}
fn makes_copy(num: i32) {
println!("{}", num);
}


This might seem strict at first, but it almost entirely eliminates issues like double frees, memory leaks, or use-after-free bugs.


Borrowing: References Without Ownership


Rust offers borrowing, allowing you to pass references to data without giving up ownership. Borrowing can be either immutable or mutable:

  • Immutable references (&T): Many references can coexist, but the data cannot be changed.
  • Mutable reference (&mut T): Only one mutable reference is allowed at a time, preventing data races.


Example:

fn main() {
let mut s = String::from("rustacean");
print_length(&s);
mutate_string(&mut s);
}
fn print_length(s: &String) {
println!("Length: {}", s.len());
}
fn mutate_string(s: &mut String) {
s.push_str("s are cool!");
}

These restrictions enforce memory safety at compile time.


Lifetimes: Preventing Dangling References


Lifetimes tell the Rust compiler how long references should be valid. They are crucial when your code involves complex borrowing, such as returning references from functions.


A classic situation:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}


Here, 'a is a lifetime parameter, indicating that both arguments and the return value must share the same lifetime. This prevents you from returning a reference to data that might not live long enough, ensuring you never have dangling refs.


Additional Rust Concepts That Surprised Me

Error Handling: The Power of Result and Option


Rust doesn’t use exceptions. Instead, it relies on types like Result and Option for safe error handling.

fn divide(a: i32, b: i32) -> Option<i32> {
if b == 0 {
None
} else {
Some(a / b)
}
}


Pattern Matching: Expressive and Safe


Pattern matching with match statements lets you elegantly handle all cases:

let number = Some(7);
match number {
Some(n) if n > 5 => println!("Big number!"),
Some(n) => println!("Number: {}", n),
None => println!("No number"),
}


Modules and Visibility


Rust encourages modular code. You can split logic across files or modules, using pub to control visibility:


mod greetings {
pub fn hello() {
println!("Hello, world!");
}
}
fn main() {
greetings::hello();
}


Concurrency: Fearless Multi-threading


Rust’s ownership and type system make writing concurrent programs safer. The compiler prevents data races, thanks to strict borrowing and ownership rules.


Final Thoughts


Learning Rust has fundamentally changed how I approach programming. Here are the top reasons why Rust is now one of my favorite languages:


  • Reliability by Design: Many classes of bugs are eliminated at compile time.


  • Expressiveness: Features like pattern matching, traits, and enums let you write concise, readable code.


  • Powerful Tooling: cargo takes the pain out of builds, dependencies, and testing.


  • Community and Documentation: The Rust community is friendly and the docs are outstanding.


  • Preparation for Scalable Systems: Rust encourages practices (ownership, error handling, concurrency) that lead to maintainable, robust, and scalable code.


While the learning curve is real — especially around ownership and lifetimes — the reward is a language that lets you write performant software without sacrificing safety or readability. I’m excited to keep building and exploring, confident that Rust will make me a better, more disciplined developer.