A Beginner's Journey into Rust

Preface Rust is a rising star among programming languages. Its memory safety, performance, and concurrency capabilities are major reasons why people choose it. However, Rust is not a particularly easy language to get started with. Beginners might feel overwhelmed when encountering concepts like "ownership" and "lifetime." Today, let's study Rust's core concepts together. From "Ownership" to "Borrowing": Rust’s Unique Charm Ownership and borrowing are core concepts in Rust. These two concepts might sound a bit abstract at first, but don’t worry — I’m here to walk you through them step by step. Ownership: With Power Comes Responsibility! Rust’s ownership rules are one of the language’s most distinctive features. You can think of it as "if you own something, you are responsible for it." Whenever you create a variable, Rust gives it ownership of a chunk of memory. Later, if you transfer ownership to another variable, the original variable becomes invalid. For example, look at this code: fn main() { let s1 = String::from("Hello, Rust!"); let s2 = s1; // Ownership of s1 is moved to s2, s1 becomes invalid // println!("{}", s1); // Error: s1 is no longer valid println!("{}", s2); // Correct: outputs Rust! } Here, we transfer ownership of s1 to s2. This means s1 can no longer be used. You might ask, "What if I don't want to lose s1?" Don’t worry — Rust also has a borrowing mechanism to help solve this problem. Borrowing: You Use It, I Still Own It Another key feature of Rust is borrowing, which means "you can use my stuff, but I still own it." There are two kinds of borrowing: immutable borrowing and mutable borrowing. Immutable borrowing: You can read the data but cannot modify it. Mutable borrowing: You can modify the data, but only one mutable borrow is allowed at a time to prevent data races. Let's look at an example: fn main() { let s = String::from("Hello, Rust!"); let s_ref = &s; // Immutable borrow println!("{}", s_ref); // Read the content of s let s_mut = &mut s; // Error: Cannot have immutable and mutable borrows simultaneously s_mut.push_str(" It's awesome!"); // Modify the content of s } Here, s_ref is an immutable borrow of s. You can read s freely but cannot modify it. s_mut is a mutable borrow, allowing modification. However, Rust does not allow having a mutable borrow and an immutable borrow at the same time, in order to prevent data inconsistency. Rust in Practice: Small Program Demonstration to Help You Understand Instantly So, how can we understand these Rust concepts through code? Come on, let’s write a small program to get you started quickly. fn main() { let numbers = vec![1, 2, 3, 4, 5]; // Immutable borrowing let mut sum = 0; for &num in &numbers { sum += num; } println!("Sum of numbers: {}", sum); // Output: Sum of numbers: 15 } This program traverses a Vec array and calculates its sum by using an immutable borrow. Here, &numbers passes an immutable borrow, which means we are only borrowing the array to read its elements without modifying it. This ensures both performance and memory safety. Of course! The above was just a small appetizer. To help you better understand Rust’s powerful capabilities, let's dive deeper into some practical code examples. I will write additional real-world examples covering different aspects of Rust’s features, allowing you to experience Rust’s charm through coding — and fall in love with it for good. A Simple Concurrency Model Based on Rust Rust’s concurrency features are extremely powerful. Through ownership and borrowing mechanisms, it ensures data safety even in concurrent environments. Let’s write a simple concurrent program that calculates the sum of numbers by processing multiple data chunks in parallel. Code example: use std::thread; fn main() { let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; let mut handles = vec![]; // Split data into two chunks and calculate the sum separately let chunk1 = numbers[0..5].to_vec(); let chunk2 = numbers[5..].to_vec(); // Create the first thread to process the first chunk let handle1 = thread::spawn(move || { let sum: i32 = chunk1.iter().sum(); println!("Sum of first chunk: {}", sum); sum }); // Create the second thread to process the second chunk let handle2 = thread::spawn(move || { let sum: i32 = chunk2.iter().sum(); println!("Sum of second chunk: {}", sum); sum }); handles.push(handle1); handles.push(handle2); // Wait for threads to finish and get the results let sum1 = handles[0].join().unwrap(); let sum2 = handles[1].join().unwrap(); println!("Total sum: {}", sum1 + sum2); // Output: Total sum: 55 } Code explanation: In this example, we use thread::spawn to create two threads and split the data into two

Apr 26, 2025 - 23:39
 0
A Beginner's Journey into Rust

Cover

Preface

Rust is a rising star among programming languages. Its memory safety, performance, and concurrency capabilities are major reasons why people choose it.

However, Rust is not a particularly easy language to get started with. Beginners might feel overwhelmed when encountering concepts like "ownership" and "lifetime." Today, let's study Rust's core concepts together.

From "Ownership" to "Borrowing": Rust’s Unique Charm

Ownership and borrowing are core concepts in Rust. These two concepts might sound a bit abstract at first, but don’t worry — I’m here to walk you through them step by step.

Ownership: With Power Comes Responsibility!

Rust’s ownership rules are one of the language’s most distinctive features. You can think of it as "if you own something, you are responsible for it." Whenever you create a variable, Rust gives it ownership of a chunk of memory. Later, if you transfer ownership to another variable, the original variable becomes invalid.

For example, look at this code:

fn main() {
    let s1 = String::from("Hello, Rust!");
    let s2 = s1; // Ownership of s1 is moved to s2, s1 becomes invalid
    // println!("{}", s1); // Error: s1 is no longer valid
    println!("{}", s2); // Correct: outputs Rust!
}

Here, we transfer ownership of s1 to s2. This means s1 can no longer be used. You might ask, "What if I don't want to lose s1?" Don’t worry — Rust also has a borrowing mechanism to help solve this problem.

Borrowing: You Use It, I Still Own It

Another key feature of Rust is borrowing, which means "you can use my stuff, but I still own it." There are two kinds of borrowing: immutable borrowing and mutable borrowing.

  • Immutable borrowing: You can read the data but cannot modify it.
  • Mutable borrowing: You can modify the data, but only one mutable borrow is allowed at a time to prevent data races.

Let's look at an example:

fn main() {
    let s = String::from("Hello, Rust!");

    let s_ref = &s; // Immutable borrow
    println!("{}", s_ref); // Read the content of s

    let s_mut = &mut s; // Error: Cannot have immutable and mutable borrows simultaneously
    s_mut.push_str(" It's awesome!"); // Modify the content of s
}

Here, s_ref is an immutable borrow of s. You can read s freely but cannot modify it. s_mut is a mutable borrow, allowing modification. However, Rust does not allow having a mutable borrow and an immutable borrow at the same time, in order to prevent data inconsistency.

Rust in Practice: Small Program Demonstration to Help You Understand Instantly

So, how can we understand these Rust concepts through code? Come on, let’s write a small program to get you started quickly.

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];

    // Immutable borrowing
    let mut sum = 0;
    for &num in &numbers {
        sum += num;
    }

    println!("Sum of numbers: {}", sum);  // Output: Sum of numbers: 15
}

This program traverses a Vec array and calculates its sum by using an immutable borrow. Here, &numbers passes an immutable borrow, which means we are only borrowing the array to read its elements without modifying it. This ensures both performance and memory safety.

Of course! The above was just a small appetizer. To help you better understand Rust’s powerful capabilities, let's dive deeper into some practical code examples. I will write additional real-world examples covering different aspects of Rust’s features, allowing you to experience Rust’s charm through coding — and fall in love with it for good.

A Simple Concurrency Model Based on Rust

Rust’s concurrency features are extremely powerful. Through ownership and borrowing mechanisms, it ensures data safety even in concurrent environments. Let’s write a simple concurrent program that calculates the sum of numbers by processing multiple data chunks in parallel.

Code example:

use std::thread;

fn main() {
    let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    let mut handles = vec![];

    // Split data into two chunks and calculate the sum separately
    let chunk1 = numbers[0..5].to_vec();
    let chunk2 = numbers[5..].to_vec();

    // Create the first thread to process the first chunk
    let handle1 = thread::spawn(move || {
        let sum: i32 = chunk1.iter().sum();
        println!("Sum of first chunk: {}", sum);
        sum
    });

    // Create the second thread to process the second chunk
    let handle2 = thread::spawn(move || {
        let sum: i32 = chunk2.iter().sum();
        println!("Sum of second chunk: {}", sum);
        sum
    });

    handles.push(handle1);
    handles.push(handle2);

    // Wait for threads to finish and get the results
    let sum1 = handles[0].join().unwrap();
    let sum2 = handles[1].join().unwrap();

    println!("Total sum: {}", sum1 + sum2); // Output: Total sum: 55
}

Code explanation:

In this example, we use thread::spawn to create two threads and split the data into two chunks to calculate separately. In Rust, the move keyword ensures that data ownership is safely transferred into the thread, thereby avoiding race conditions.

Error Handling in Rust: Result and Option Types

Rust’s error handling mechanism is different from traditional programming languages. Rust provides Result and Option types to handle possible errors and null values. Let's see how to use them.

Code example:

fn divide(dividend: i32, divisor: i32) -> Result<i32, String> {
    if divisor == 0 {
        Err(String::from("Cannot divide by zero"))
    } else {
        Ok(dividend / divisor)
    }
}

fn main() {
    match divide(10, 2) {
        Ok(result) => println!("Result: {}", result), // Output: Result: 5
        Err(e) => println!("Error: {}", e),
    }

    match divide(10, 0) {
        Ok(result) => println!("Result: {}", result),
        Err(e) => println!("Error: {}", e), // Output: Error: Cannot divide by zero
    }
}

Code explanation:

In this example, we define a divide function that returns a Result type. Result has two variants: Ok representing a successful result, and Err representing an error. When the divisor is zero, it returns an error message. In the main function, we use match to handle these two possibilities.

Handling Null Values with Option:

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

fn main() {
    let numbers = vec![1, 3, 5, 7, 8, 11];
    match find_first_even(numbers) {
        Some(even) => println!("First even number: {}", even), // Output: First even number: 8
        None => println!("No even number found"),
    }

    let empty = vec![1, 3, 5, 7];
    match find_first_even(empty) {
        Some(even) => println!("First even number: {}", even),
        None => println!("No even number found"), // Output: No even number found
    }
}

Code explanation:

In this example, the Option type is used to represent a value that may or may not exist. Some wraps a value, while None represents the absence of a value. The find_first_even function returns an Option type, indicating that it may find an even number or it may not.

Creating Custom Data Types with struct and impl in Rust

Rust’s struct and impl provide a very powerful way to define and operate on custom data types. Here’s a simple example showing how to create a custom Point struct and implement related methods.

Code example:

#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

impl Point {
    // Create a new Point instance
    fn new(x: i32, y: i32) -> Self {
        Point { x, y }
    }

    // Calculate the distance from the origin
    fn distance_from_origin(&self) -> f64 {
        ((self.x.pow(2) + self.y.pow(2)) as f64).sqrt()
    }

    // Display the point’s coordinates
    fn display(&self) {
        println!("Point({}, {})", self.x, self.y);
    }
}

fn main() {
    let p1 = Point::new(3, 4);
    let p2 = Point::new(6, 8);

    p1.display(); // Output: Point(3, 4)
    p2.display(); // Output: Point(6, 8)

    let distance = p1.distance_from_origin();
    println!("Distance from origin: {}", distance); // Output: Distance from origin: 5.0
}

Code explanation:

In this example, we define a Point struct containing two fields, x and y. Then, through an impl block, we provide several methods for Point: new to create a new Point, distance_from_origin to calculate the distance from the origin, and display to print the coordinates.

Simple File Operations: Reading and Writing Files in Rust

Rust provides powerful file operation capabilities, allowing us to easily read and write files. Here's a simple example showing how to read a text file and write its contents to another file.

Code example:

use std::fs::{File, OpenOptions};
use std::io::{self, Read, Write};

fn read_file(path: &str) -> io::Result<String> {
    let mut file = File::open(path)?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(contents)
}

fn write_file(path: &str, data: &str) -> io::Result<()> {
    let mut file = OpenOptions::new()
        .create(true)
        .write(true)
        .open(path)?;
    file.write_all(data.as_bytes())?;
    Ok(())
}

fn main() -> io::Result<()> {
    let input_path = "input.txt";
    let output_path = "output.txt";

    // Read file contents
    let content = read_file(input_path)?;
    println!("File content: \n{}", content);

    // Write contents to another file
    write_file(output_path, &content)?;

    Ok(())
}

Code explanation:

In this example, the read_file function reads the contents of a file and returns it as a String. The write_file function writes data to the specified file. If the file does not exist, it will automatically be created. We use the ? operator to simplify error handling — Rust will automatically propagate the error if any operation fails.

Through these code examples, I have demonstrated practical exercises in Rust concerning concurrency, error handling, data structures, and file operations. Rust’s design enables us to write code with high safety and performance while enjoying a clear and concise syntax. If you can master these basic features, developing a Rust project will become much more manageable!

The Integration of Rust with Other Languages

Rust’s charm lies not only in its standalone power but also in its excellent integration with other technologies. For example, in combination with Node.js, you can use Rust to write high-performance, low-level code, while using Node.js to handle higher-level logic. This complementary approach leverages Rust’s efficiency and safety advantages and fully utilizes Node.js’s flexibility and maturity in certain fields.

Rust’s Innovative Exploration in Emerging Fields

Rust’s performance in various emerging fields is exciting. For instance, many blockchain projects are using Rust because it can ensure the blockchain system remains stable and fast even under heavy loads. Moreover, Rust’s memory safety and concurrency features can prevent potential security issues, ensuring data accuracy and consistency.

Conclusion

Rust is a language that is both rigorous and efficient. It eliminates hidden dangers of memory safety in program development while maintaining outstanding performance.

Although its learning curve is a bit steep, what could be more exciting than mastering a powerful new language?

We are Leapcell, your top choice for hosting Rust projects.

Leapcell

Leapcell is the Next-Gen Serverless Platform for Web Hosting, Async Tasks, and Redis:

Multi-Language Support

  • Develop with Node.js, Python, Go, or Rust.

Deploy unlimited projects for free

  • pay only for usage — no requests, no charges.

Unbeatable Cost Efficiency

  • Pay-as-you-go with no idle charges.
  • Example: $25 supports 6.94M requests at a 60ms average response time.

Streamlined Developer Experience

  • Intuitive UI for effortless setup.
  • Fully automated CI/CD pipelines and GitOps integration.
  • Real-time metrics and logging for actionable insights.

Effortless Scalability and High Performance

  • Auto-scaling to handle high concurrency with ease.
  • Zero operational overhead — just focus on building.

Explore more in the Documentation!

Try Leapcell

Follow us on X: @LeapcellHQ

Read on our blog