Rust · lesson 2 of 30
Beginner
15- 1 Introduction to Rust
- 2 Setting Up Rust
- 3 Variables in Rust
- 4 Data Types in Rust
- 5 Operators in Rust
- 6 Control Flow in Rust
- 7 Functions in Rust
- 8 Ownership in Rust
- 9 Borrowing and References
- 11 Structs in Rust
- 12 Enums in Rust
- 13 Pattern Matching in Rust
- 17 Collections in Rust
- 25 Testing in Rust
- 26 File I/O in Rust
Intermediate
9Advanced
6Setting Up Rust
Install Rust with rustup, configure your editor, and write your first program.
By SkillByExample · Updated
Installing Rust with rustup
The official and recommended way to install Rust is through rustup, a toolchain manager that handles installation, updates, and switching between stable/beta/nightly channels. Installing via rustup is strongly preferred over OS package managers — it gives you the latest stable release and makes updating trivially easy, while system packages often lag months behind.
Linux / macOS
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Follow the prompts and choose the default installation. Then reload your shell so the cargo and rustc binaries are on your PATH:
source ~/.cargo/env
Windows
Download and run rustup-init.exe from https://rustup.rs. The installer also prompts you to install the MSVC C++ build tools, which are required on Windows because the Rust compiler links against the system C runtime.
Verify the Installation
After installation, confirm all three tools are available and check their versions:
rustc --version # rustc 1.78.0 (9b00956e5 2024-04-29)
cargo --version # cargo 1.78.0 (54d8815d0 2024-03-26)
rustup --version # rustup 1.27.0 (bbb9276d2 2024-03-08)
Understanding the Toolchain
Rust ships as an integrated toolchain where each component has a specific role. Understanding what each tool does will save you confusion as your projects grow.
rustup — manages Rust versions (like nvm for Node)
rustc — the Rust compiler
cargo — build system + package manager + test runner
rustfmt — code formatter (cargo fmt)
clippy — linter (cargo clippy)
rust-docs — offline docs (rustup doc)
Keeping Rust Up to Date
Rust follows a six-week release cycle. Updating takes one command:
rustup update # update all installed toolchains
rustup update stable # update only stable
Installing Nightly (optional)
Some features (like certain proc-macro APIs or experimental syntax) require nightly. You can install it alongside stable and use it only in specific directories:
rustup toolchain install nightly
rustup override set nightly # use nightly in current directory only
Setting Up VS Code
A good editor setup pays dividends immediately — real-time error feedback from rust-analyzer catches mistakes before you run cargo build, which speeds up the learning loop significantly.
- Install VS Code
- Install the rust-analyzer extension (extension id:
rust-lang.rust-analyzer) - Optionally install Even Better TOML for
Cargo.tomlsupport - Optionally install CodeLLDB for a native debugger
rust-analyzer provides:
- Real-time error highlighting
- Auto-complete and signature help
- Go-to-definition and find-references
- Inline type hints
- Automatic imports
A minimal VS Code settings.json for Rust that runs Clippy on save and enables helpful inlay hints:
{
"rust-analyzer.checkOnSave.command": "clippy",
"rust-analyzer.inlayHints.parameterHints.enable": true,
"rust-analyzer.inlayHints.typeHints.enable": true,
"[rust]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "rust-lang.rust-analyzer"
}
}
Creating Your First Project with Cargo
Cargo is not just a build tool — it is the central hub of the Rust development experience. Every Rust project is a Cargo project. It handles dependencies, compilation, testing, documentation, and publishing to crates.io.
cargo new hello_rust
cd hello_rust
Cargo generates a ready-to-use project structure:
hello_rust/
├── Cargo.toml # project manifest — name, version, dependencies
└── src/
└── main.rs # entry point — where execution begins
Cargo.toml — the project manifest. This is where you declare your package metadata and dependencies:
[package]
name = "hello_rust"
version = "0.1.0"
edition = "2024"
[dependencies]
src/main.rs — the generated starter code:
fn main() {
println!("Hello, world!");
}
Building and Running
Cargo provides a handful of commands you will use constantly. The distinction between debug and release builds matters — debug builds compile fast but include no optimisations, while release builds are slower to compile but produce fast, production-ready binaries.
cargo run # compile + run (debug build) — fastest compile time
cargo build # compile only (debug)
cargo build --release # optimised release build — always benchmark with this
cargo check # type-check without producing a binary (fastest feedback)
Always benchmark and profile with --release — debug binaries can be 10-100x slower for compute-heavy code.
A Slightly More Interesting First Program
This example reads your name from stdin and greets you. It introduces a few real concepts: user input, shadowing, and the format! macro. Replace src/main.rs with:
use std::io::{self, Write};
// Returns a formatted greeting string — note the &str parameter type
fn greet(name: &str) -> String {
format!("Hello, {}! Welcome to Rust.", name)
}
fn main() {
print!("Enter your name: ");
io::stdout().flush().unwrap(); // flush so prompt appears before we wait
let mut name = String::new();
io::stdin().read_line(&mut name).unwrap(); // read into a mutable String
let name = name.trim(); // shadow with trimmed version — drops leading/trailing whitespace
println!("{}", greet(name));
}
cargo run
# Enter your name: Alice
# Hello, Alice! Welcome to Rust.
Adding Dependencies
One of Cargo’s biggest strengths is how easy it is to add third-party libraries. Dependencies are declared in Cargo.toml and downloaded automatically from crates.io on the next build.
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
Or use the cargo add command directly from the terminal — it updates Cargo.toml for you:
cargo add serde --features derive
cargo add serde_json
Then run cargo build to fetch and compile them. Cargo.lock records the exact resolved versions so builds are reproducible across machines.
Useful Cargo Commands
These commands cover the full day-to-day development cycle. Becoming fluent with them will make you significantly more productive.
cargo test # run all tests
cargo doc --open # build and open documentation
cargo fmt # format all source files
cargo clippy # run the linter — catches common mistakes
cargo clean # remove build artifacts
cargo tree # show dependency tree
cargo update # update dependencies within semver bounds
Offline Documentation
One underappreciated feature of the Rust toolchain is that the entire documentation set is available offline after installation. No internet required, and the docs are always in sync with your installed version.
rustup doc # open the Rust book, std library docs, etc.
rustup doc --std # open standard library docs only
rustup doc --book # open The Rust Programming Language book
Project Structure for Larger Projects
As projects grow, Cargo supports a conventional layout for tests, benchmarks, and examples. Knowing this structure upfront helps you organise code from the start.
my_project/
├── Cargo.toml
├── Cargo.lock # committed for binaries, gitignored for libraries
├── src/
│ ├── main.rs # binary entry point
│ ├── lib.rs # library root (optional)
│ └── modules/
│ └── mod.rs
├── tests/
│ └── integration_test.rs
├── benches/
│ └── my_bench.rs
└── examples/
└── demo.rs
Run a specific example:
cargo run --example demo
You are now ready to write Rust. The next tutorial covers variables, mutability, and constants.
Practice
- Run it twice. Create
hello_rustwithcargo new, run it withcargo run, then run it again straight away. What does Cargo skip the second time?
Solution
$ cargo new hello_rust
Creating binary (application) `hello_rust` package
note: see more `Cargo.toml` keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
$ cd hello_rust
$ cargo run
Compiling hello_rust v0.1.0 (/src/hello_rust)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.58s
Running `target/debug/hello_rust`
Hello, world!
$ cargo run
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.28s
Running `target/debug/hello_rust`
Hello, world!
$ cat Cargo.toml
[package]
name = "hello_rust"
version = "0.1.0"
edition = "2024"
[dependencies]
The second run has no Compiling line, because Cargo saw that nothing in src/ had changed and reused
the binary in target/debug/. The dev profile is the debug build, unoptimised and with debug info. The
generated manifest uses the newest edition your toolchain supports, which is 2024 on any Rust from
1.85 onwards.
- Greet strangers too. Change the name program so that pressing Enter without typing a name prints
Hello, stranger! Welcome to Rust., and any other name gets its length in bytes. Pipe input in withechoinstead of typing it.
Solution
use std::io::{self, Write};
fn greet(name: &str) -> String {
if name.is_empty() {
return String::from("Hello, stranger! Welcome to Rust.");
}
format!("Hello, {}! Your name is {} bytes long.", name, name.len())
}
fn main() {
print!("Enter your name: ");
io::stdout().flush().unwrap();
let mut name = String::new();
io::stdin().read_line(&mut name).unwrap();
let name = name.trim();
println!("{}", greet(name));
}
$ echo "Alice" | cargo run -q
Enter your name: Hello, Alice! Your name is 5 bytes long.
$ echo " Bob " | cargo run -q
Enter your name: Hello, Bob! Your name is 3 bytes long.
$ echo "" | cargo run -q
Enter your name: Hello, stranger! Welcome to Rust.
Piped input isn’t echoed to the screen, so the greeting appears on the prompt’s line. trim() removes
the spaces around Bob and the trailing newline, which is why an empty line becomes an empty &str. -q
hides Cargo’s own status lines.
- Let
cargo checkfind a type error. Callgreet(42)instead of passing a string, and runcargo checkrather thancargo run.
Solution
fn greet(name: &str) -> String {
format!("Hello, {}! Welcome to Rust.", name)
}
fn main() {
println!("{}", greet(42));
}
$ cargo check
Checking typo v0.1.0 (/src)
error[E0308]: mismatched types
--> src/main.rs:6:26
|
6 | println!("{}", greet(42));
| ----- ^^ expected `&str`, found integer
| |
| arguments to this function are incorrect
|
note: function defined here
--> src/main.rs:1:4
|
1 | fn greet(name: &str) -> String {
| ^^^^^ ----------
For more information about this error, try `rustc --explain E0308`.
error: could not compile `typo` (bin "typo") due to 1 previous error
cargo check runs the full type checker but skips code generation, which makes it the quickest way to
see errors like this. rust-analyzer runs the same check as you type. The fix is greet("42"), or
greet(&42.to_string()) if you really have a number.
-
Let Clippy tidy it up. This version compiles and runs correctly. Run
cargo clippyon it and apply what it suggests.fn greet(name: &str) -> String { if name.len() == 0 { return String::from("Hello, stranger!"); } return format!("Hello, {}!", name); } fn main() { println!("{}", greet("")); println!("{}", greet("Alice")); }
Solution
fn greet(name: &str) -> String {
if name.is_empty() {
return String::from("Hello, stranger!");
}
format!("Hello, {}!", name)
}
fn main() {
println!("{}", greet(""));
println!("{}", greet("Alice"));
}
$ cargo clippy
Checking lint v0.1.0 (/src)
warning: unneeded `return` statement
--> src/main.rs:5:5
|
5 | return format!("Hello, {}!", name);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.99.0/index.html#needless_return
= note: `#[warn(clippy::needless_return)]` on by default
help: remove `return`
|
5 - return format!("Hello, {}!", name);
5 + format!("Hello, {}!", name)
|
warning: length comparison to zero
--> src/main.rs:2:8
|
2 | if name.len() == 0 {
| ^^^^^^^^^^^^^^^ help: using `is_empty` is clearer and more explicit: `name.is_empty()`
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.99.0/index.html#len_zero
= note: `#[warn(clippy::len_zero)]` on by default
warning: `lint` (bin "lint") generated 2 warnings (run `cargo clippy --fix --bin "lint" -p lint -- ` to apply 2 suggestions)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
$ cargo clippy
Checking lint v0.1.0 (/src)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.53s
Neither warning is a bug. Clippy flags code that works but isn’t idiomatic Rust. The last expression in a
function is its return value, so the final return is redundant, while the early return stays because
it leaves the function before the end. If clippy isn’t installed, run rustup component add clippy.
- Add an example target. Create
examples/demo.rsthat prints the command-line arguments it receives, then run it withcargo run --example demo, both with and without arguments.
Solution
fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
println!("demo example running");
println!("got {} argument(s): {:?}", args.len(), args);
}
$ cargo run --example demo
Compiling hello_rust v0.1.0 (/src)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.31s
Running `target/debug/examples/demo`
demo example running
got 0 argument(s): []
$ cargo run -q --example demo -- one two
demo example running
got 2 argument(s): ["one", "two"]
Every file in examples/ is its own small program with its own main, built into
target/debug/examples/. It can use the package’s library code but isn’t part of the main binary.
Anything after -- is passed to the program instead of to Cargo, and .skip(1) drops the program’s own
path, which is always the first argument.