7 September 2023
Rust is a systems programming language that has quickly grown in popularity in recent years. Developed by Mozilla Research, Rust combines speed, safety, and concurrency in one fast systems language.
In this comprehensive, 3-hour crash course article, you'll learn Rust from the ground up through hands-on examples and code samples. We'll cover everything from installing Rust and writing your first Rust program to more advanced topics like ownership, borrowing, and lifetimes.
By the end, you'll have a solid grasp of Rust syntax, how to structure and build Rust projects, and be well on your way to leveraging Rust's key strengths like speed, safety, and fearless concurrency.
This guide is intended as a support tool to our Rust course. get in contact to enroll or discover how we can supply a training solution for you.
Let's get Rusting!
As a beginner, you may be wondering — why should I invest time in learning Rust vs other popular languages like JavaScript or Python? Here are some of the key reasons Rust is worth learning:
So in summary, Rust gives you both high-level ergonomics as well as low-level control, making it a great choice for systems programming. Let's look next at how to setup a Rust programming environment.
The easiest way to install Rust is via rustup, a command line tool for managing Rust versions and associated tools.
To install rustup and the latest version of Rust, run this in your terminal:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
This will install rustup and add the latest stable Rust toolchain to your system PATH.
To verify it worked, you can check the installed version:
rustc --version
Rustup also installs the Rust package manager, cargo, which you will use to create, build, and manage Rust projects. Check it is installed with:
cargo --version
With rustup, installing Rust also installs the standard library and compiler. Additionally, you can install debugging tools, IDE integration, and more via rustup components:
rustup component add rust-docs --toolchain nightly
rustup component add rust-analysis --toolchain nightly
Now you are ready to start coding in Rust!
Traditionally, the first program beginners write in a new language prints "Hello, world!" to the terminal. Here is how you would do this in Rust:
fn main() {
println!("Hello, world!");
}
Let's break down what's happening in this small program:
fn main()
defines a function called main — this is the entry point of all Rust programs.println!
is a Rust macro that prints text to the console."Hello, world!"
is a Rust string literal.To run the program, save the code in a file hello.rs
and compile with rustc
:
rustc hello.rs
This will create a binary hello
that you can then execute:
./hello
Which prints "Hello, world!" - your first Rust program!
In the next sections, we'll cover more Rust syntax basics including variables, types, functions, if/else expressions, and more.
Now that you've written a simple Rust program, let's go over the fundamental building blocks of Rust code.
In Rust, by default variables are immutable. This means once bound to a value, a variable can't be changed.
let x = 5;
x = 6; // error!
To make a variable mutable, use mut
:
let mut x = 5; // mutable variable
x = 6; // ok!
Constants are also supported, which can never change value once defined:
const API_KEY: &str = "abcd1234"; // constant
Rust is a statically typed language, meaning variables have a defined type set at compile time.
Some common Rust primitive types include:
bool
- Boolean type with values true
and false
i32
- 32-bit signed integeru32
- 32-bit unsigned integerf32
, f64
- 32 and 64-bit floating point numberschar
- Single unicode characterstr
- String sliceFor example:
let is_true: bool = true;
let meaning: i32 = 42;
let pi: f64 = 3.141592;
let initial: char = 'C';
let name: &str = "Carol";
Tuples can group values of different types:
let tup: (i32, &str, bool) = (50, "Ferris", false);
And arrays collect values of the same type:
let nums: [i32; 5] = [1, 2, 3, 4, 5];
Functions in Rust are declared with fn
:
fn double(x: i32) -> i32 {
x * 2
}
Let's break this down:
fn
declares a new functiondouble
- the function name(x: i32)
- the function parameter x
of type i32
-> i32
- the return type, also an i32
{ x * 2 }
- the function body, it doubles and returns x
We can call the function like:
let result = double(5); // result = 10
Functions are used extensively in Rust to abstract away logic.
Rust has familiar flow control like if
/else
expressions:
let num = 5;
if num == 5 {
println!("Number is 5");
} else if num == 6 {
println!("Number is 6");
} else {
println!("Number is not 5 or 6");
}
As well as for
loops that iterate over ranges or collection types:
for x in 1..11 {
println!("{}", x); // print 1 to 10
}
This covers some of the basics of Rust syntax! Next we'll look at how Rust handles data allocation.
A key aspect of Rust is its ownership model for managing memory. Understanding Rust's ownership system is critical to writing safe Rust programs.
In Rust, each value has a variable that is its owner. There can only be one owner at a time. When the owner goes out of scope, the value will be dropped.
This ownership model ensures memory safety without needing a garbage collector.
For example:
{
let s = "hello".to_string(); // s owns the String value
println!("{}", s); // use the String
} // s goes out of scope and is dropped
Some key ownership rules:
Values in Rust can be stored on the stack or the heap:
String
, variable size, stored as a pointer.Heap allocation must also be deallocated when the owner goes out of scope. Rust automatically frees heap data once the owner is no longer used, without needing a garbage collector.
Rust allows you to have multiple references to data via borrowing:
let s = String::from("hello"); // s owns the string
let len = calculate_length(&s); // borrow s
&s
passes a reference to s
without transferring ownership.s
is in scope.Borrowing has a few rules:
This borrowing system prevents data races and ensures memory safety.
Now that you understand ownership and borrowing, let's look at how data structures like structs and enums are defined in Rust.
A struct
allows you to create a custom data type:
struct Book {
title: String,
author: String,
pages: i32
}
let the_rust_book = Book {
title: "The Rust Programming Language".to_string(),
author: "Steve Klabnik".to_string(),
pages: 532,
};
Instances of a struct are created by specifying values for each field.
Methods can be added to structs via impl
:
impl Book {
fn summary(&self) {
println!("{} ({} pages) by {}", self.title, self.pages, self.author);
}
}
let b = Book { /* fields */ };
b.summary(); // call method
Enums allow you to define a type with distinct variants:
enum Animal {
Dog,
Cat,
Monkey,
}
let a = Animal::Dog;
Each variant can store data:
enum Shape {
Rectangle { width: u32, height: u32},
Circle(u32),
}
let s = Shape::Circle(7);
Enums are commonly used with match
to branch based on the variant:
fn animal_sound(a: Animal) -> &'static str {
match a {
Animal::Dog => "bark!",
Animal::Cat => "meow!",
Animal::Monkey => "ooooh ooooh ah ah",
}
}
This covers some of the ways you can organize data in Rust. Next we'll look at error handling.
Errors are inevitable in real-world programs. Rust has first-class support for error handling through Result
and panic!
.
The Result<T, E>
enum is used to indicate an operation that may succeed (Ok
) or fail (Err
):
enum Result<T, E> {
Ok(T),
Err(E),
}
For example, parsing a string into an integer could be written as:
fn parse_int(s: &str) -> Result<i32, ParseIntError> {
match s.parse::<i32>() {
Ok(i) => Ok(i),
Err(e) => Err(ParseIntError),
}
}
Now callers must handle the Result
properly:
match parse_int("42") {
Ok(i) => println!("{}", i),
Err(e) => println!("Error: {}", e),
}
This prevents errors from being ignored.
When the program reaches an unrecoverable state, you can call panic!
:
fn check(i: i32) {
if i == 0 {
panic
You're absolutely right, my apologies. Here is the rest of the article from where I left off:
1 / i;
}
A panic will print a failure message, unwind the stack, and quit.
Panic should only be used for exceptional errors, not control flow. Rust's robust error handling prevents many panics.
We've covered the basics so far. Now let's discuss some of Rust's advanced concepts that really set it apart: generics, traits, and concurrency.
Generics allow code to be abstracted over different types:
struct Point<T> {
x: T,
y: T,
}
let int_point = Point{ x: 10, y: 20 };
let float_point = Point{ x: 1.2, y: 4.5 };
The generic type T
is substituted with concrete types at compile time.
Traits are similar to interfaces in other languages. They define shared behavior that types can implement:
trait Log {
fn log(&self);
}
struct Person { name: String }
impl Log for Person {
fn log(&self) {
println!("My name is {}", self.name);
}
}
let person = Person{ name: "John".to_string() };
person.log();
Traits allow code reuse across many types.
Rust provides multiple concurrency primitives:
Threads
use std::thread;
thread::spawn(|| {
// code in this thread executes concurrently
});
Channels
use std::sync::mpsc;
let (tx, rx) = mpsc::channel();
tx.send("hi").unwrap();
let msg = rx.recv();
Channels allow threads to communicate without sharing memory.
Mutexes
use std::sync::Mutex;
let m = Mutex::new(0);
{
let mut val = m.lock().unwrap();
*val += 1;
}
Mutexes allow concurrent access to shared data. Rust prevents data races at compile time.
This gives a brief intro to some of Rust's advanced features for generics, traits, and concurrency. Let's now look at building real Rust projects.
Cargo is Rust's built-in package manager and build tool. It lets you manage dependencies and build/test Rust projects easily.
To create a new Rust project, use cargo new
:
cargo new my-project
Created binary (application) `my-project` package
This generates a simple project with the following files:
my-project
|- Cargo.toml
|- src
|- main.rs
Cargo.toml
contains project metadata and dependencies.src/main.rs
contains the project code.You can include external crates from crates.io by adding them under [dependencies]
in Cargo.toml:
[dependencies]
ferris-says = "0.2"
Then import and use the crate in main.rs
:
extern crate ferris_says;
use ferris_says::say;
fn main() {
say("Hello fellow Rustaceans!");
}
Cargo handles downloading and building the dependencies automatically.
Use cargo build
to compile your project:
cargo build
Compiling my-project v0.1.0
Then run the compiled executable with cargo run
:
cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/my-project`
Hello fellow Rustaceans!
That covers the basics of creating, building, and running a Rust project locally!
I hope this introduction has shown you how Rust provides both high-level ergonomics as well as low-level control. Your next step on the journey is to build something hands-on!
Here are some ideas for where to go from here:
The Rust language has a great ecosystem of tools, libraries, and training resources. Welcome to the community! Rust's combination of performance, safety, and productivity make it a joy to use.
Rust Training is just one of the courses you can enroll in with JBI Training you might also be interested in
CONTACT
+44 (0)20 8446 7555
Copyright © 2024 JBI Training. All Rights Reserved.
JB International Training Ltd - Company Registration Number: 08458005
Registered Address: Wohl Enterprise Hub, 2B Redbourne Avenue, London, N3 2BS
Modern Slavery Statement & Corporate Policies | Terms & Conditions | Contact Us