This project was created to be used as a learning support for those who want to try Rust lang.
To get started with Rust, you need to have Rust installed on your machine.
Please refer to the official Rust installation guide: https://rust-lang.org/learn/get-started/
We recommend using Visual Studio Code with the Rust Analyzer extension for a better development experience.
https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer
Use the let keyword to declare variables.
let age = 39;
let pi = 3.14;
let name = "XPELAB";
let is_active = true;In Rust, variable types can be explicitly declared by annotating them after the variable name.
let age: i32 = 39;
let pi: f64 = 3.14;
let name: &str = "XPELAB";
let is_active: bool = true;Enums are a way to define a type that can be one of several variants.
enum Direction {
Up,
Down,
Left,
Right,
}
let dir: Direction = Direction::Up;Enums can have associated data. Data of each variant can be different.
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
let msg: Message = Message::Move { x: 10, y: 20 };Sequences are collections of values of the same type.
let numbers: [i32; 5] = [1, 2, 3, 4, 5]; // Array
let names: Vec<&str> = vec!["Alice", "Bob", "Charlie"]; // VectorSlices are a view into a contiguous sequence of elements in a collection.
let numbers: [i32; 5] = [1, 2, 3, 4, 5];
let slice: &[i32] = &numbers[1..4]; // Slice containing elements 2, 3, 4Include slice boundary examples:
let slice_from_start: &[i32] = &numbers[..3]; // Slice containing elements 1, 2, 3
let slice_to_end: &[i32] = &numbers[2..]; // Slice containing elements 3, 4, 5
let full_slice: &[i32] = &numbers[..]; // Slice containing all elements
let full_slice_with_equal: &[i32] = &numbers[0..=4]; // Slice containing all elements using =Tuples are a way to group multiple values into a single compound value.
let person: (&str, i32, bool) = ("Alice", 30, true);
let (name, age, is_active) = person; // DestructuringBy default, variables in Rust are immutable. To make a variable mutable, use the mut keyword.
let mut age = 39;
age = 40; // This is allowed because 'age' is mutableOwnership is a key feature in Rust that ensures memory safety without a garbage collector. Each value in Rust has a single owner, and when the owner goes out of scope, the value is dropped.
When you assign a value to a new variable, the ownership of that value is moved to the new variable.
let s1 = String::from("hello");
let s2 = s1; // s1 is moved to s2, s1 is no longer validBorrowing allows you to reference a value without taking ownership of it. You can borrow a value using references.
let s1 = String::from("hello");
let len = calculate_length(&s1); // Borrowing s1let age: i32 = 30; // Immutable variable
let mut score: i32 = 100; // Mutable variable
let age_reference: &i32 = &age; // Immutable reference
let mutable_score_reference: &mut i32 = &mut score; // Mutable referenceControl flow in Rust is managed using conditional statements and loops.
let number = 5;
if number < 10 {
println!("The number is less than 10");
} else if number == 10 {
println!("The number is equal to 10");
} else {
println!("The number is greater than 10");
}let mut count = 0;
loop {
count += 1;
if count == 5 {
break;
}
}let mut number = 3;
while number != 0 {
println!("{}!", number);
number -= 1;
}
println!("Liftoff!");let a = [10, 20, 30, 40, 50];
for element in a.iter() {
println!("The value is: {}", element);
}let number = 13;
match number {
1 => println!("One"),
2 | 3 | 5 | 7 | 11 => println!("This is a prime"),
13..=19 => println!("A teen"),
_ => println!("A number"),
}Functions in Rust are declared using the fn keyword.
fn greet(name: &str) {
println!("Hello, {}!", name);
}Functions can take parameters and return values.
fn add(a: i32, b: i32) -> i32 {
a + b // Equivalent to return a + b;
}Options in Rust are used to represent a value that can either be present (Some) or absent (None).
fn divide(a: f64, b: f64) -> Option<f64> {
if b == 0.0 {
None
} else {
Some(a / b)
}
}Results in Rust are used for error handling. A Result can be either Ok (indicating success) or Err (indicating failure).
fn read_file(filename: &str) -> Result<String, std::io::Error> {
std::fs::read_to_string(filename)
}
fn main() {
match read_file("hello.txt") {
Ok(contents) => println!("File contents: {}", contents),
Err(e) => println!("Error reading file: {}", e),
}
}Panic is a way to handle unrecoverable errors in Rust. When a panic occurs, the program will terminate.
panic!("Something went wrong!");Recoverable errors can be handled using the Result type.
fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err(String::from("Division by zero"))
} else {
Ok(a / b)
}
}The ? operator is used to propagate errors in functions that return a Result type.
fn read_username_from_file() -> Result<String, std::io::Error> {
let mut file = std::fs::File::open("username.txt")?;
let mut username = String::new();
file.read_to_string(&mut username)?;
Ok(username)
}Scopes in Rust are defined by curly braces {}. Variables are valid only within the scope they are declared.
let x = 10;
{
let x = 5;
println!("x is: {}", x); // x is valid here
}
println!("x is: {}", x); // will print 10Closures are anonymous functions that can capture variables from their surrounding scope.
let add = |a: i32, b: i32| -> i32 {
a + b
};
let result = add(5, 3); // result is 8Structs are custom data types that let you group related data together.
struct Person {
name: String,
age: u32,
}
let person = Person {
name: String::from("Alice"),
age: 30,
};Implementation blocks (impl) are used to define methods for structs or implement method traits.
impl Person {
fn new(name: String, age: u32) -> Self {
Self { name, age }
}
fn greet(&self) {
println!("Hello, my name is {} and I am {} years old.", self.name, self.age);
}
}Multiple impl blocks can be defined for a single struct.
Traits are a way to define shared behavior in Rust. They are similar to interfaces in other languages.
trait Greet {
fn greet(&self);
}
impl Greet for Person {
fn greet(&self) {
println!("Hello, my name is {}.", self.name);
}
}Rust provides several built-in traits that can be automatically implemented for your structs using the #[derive] attribute.
#[derive(Debug, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}Generics allow you to write flexible and reusable code by defining functions, structs, or enums that can work with different types.
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}The turbo fish syntax ::<Type> is used to explicitly specify generic type parameters when calling functions or methods.
let numbers = vec![1, 2, 3, 4, 5];
let largest = largest::<i32>(&numbers);Lifetimes are a way to specify how long references are valid in Rust. They help prevent dangling references and ensure memory safety.
fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() > s2.len() {
s1
} else {
s2
}
}Modules are a way to organize code into separate namespaces.
There are 3 ways to declare modules in Rust:
- Inline modules
- File modules
- Directory modules
mod math {
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
}
let sum = math::add(5, 3);Create a file named math.rs:
pub fn add(a: i32, b: i32) -> i32 {
a + b
}In your main file:
mod math;
let sum = math::add(5, 3);Create a directory named math with a file mod.rs:
pub fn add(a: i32, b: i32) -> i32 {
a + b
}In your main file:
mod math;
let sum = math::add(5, 3);use keyword is used to bring modules, structs, functions, or traits into scope.
use math::add;
let sum = add(5, 3);A Rust project can be of 2 types: binary or library.
A library crate is a collection of code that can be shared and reused across multiple projects. It does not have a main function and cannot be executed directly.
A binary crate is an executable program that has a main function as the entry point.
Each Rust project can be either a library or a binary, or both.
Create a lib.rs file into your binary project can help you to organize your code better.
To create multiple binaries in a single Rust project, you can create a src/bin directory and add separate .rs files for each binary.
Each file in the src/bin directory will be compiled as a separate binary.
Declare a main function in each file.
Tests in Rust are written in special test modules that are annotated with the #[cfg(test)] attribute.
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
}Integration tests are placed in the tests directory at the root of your project.
// tests/integration_test.rs
use my_crate::add;
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}Crates.io is the Rust community's crate registry. It is a place where you can find and publish Rust libraries and applications.
The official website for crates.io is https://crates.io/.
Let's create a simple backend using the Axum framework and PostgreSQL database.