Learn Rust Series (#1) - Introduction to Rust
Learn Rust Series (#1) - Introduction to Rust

What will I learn
- You will learn why Rust exists and which specific problems it solves that other languages leave to programmer discipline;
- the mental model behind ownership, and why it delivers memory safety without a garbage collector;
- how to install the toolchain with rustup and create your first project with cargo;
- what a Cargo project is actually made of, and how to build, check, and run real code;
- how to read the compiler when it pushes back, and when Rust is the right tool versus overkill.
Requirements
- A working modern computer running macOS, Windows or Ubuntu;
- An installed Rust toolchain (via rustup, from rustup.rs);
- A terminal and an editor (VS Code with the rust-analyzer extension is a great start);
- The ambition to learn systems programming from the ground up.
Difficulty
- Beginner (with honest expectations about the learning curve)
Curriculum (of the Learn Rust Series):
- Learn Rust Series (#1) - Introduction to Rust (this post)
Learn Rust Series (#1) - Introduction to Rust
Welcome to a brand new series. Over the coming episodes we are going to learn Rust properly, from the ground up, and by the end you will be writing real systems software: tools, servers, parsers, the works. But we start here, at episode one, with the question that the whole language is an answer to.
Rust asks something uncomfortable: what if entire classes of bugs could be made impossible at compile time, with no runtime cost? Not caught by a linter, not usually avoided by careful review, but flatly rejected by the compiler before the program can even run. That is the promise, and the rest of this series is about earning it.
This first episode is deliberately light on syntax and heavy on the mental model, because if you understand why Rust is shaped the way it is, every later feature stops looking arbitrary and starts looking inevitable. We will still get our hands dirty though: by the end you will have the toolchain installed and a real program compiling and running.
Nota bene: if you already know Python or Go, keep them in mind as we go. I will compare against them often, because the contrast is the fastest way to see what Rust is actually doing differently.
The problem Rust solves
C and C++ run the world's systems software: operating systems, browsers, databases, game engines, network stacks. They give you total control over memory and performance. That control has a price, and the price is paid in security bugs. Microsoft and Google have both reported that roughly 70% of the serious vulnerabilities in their large C/C++ codebases come from memory safety mistakes.
The list of those mistakes is depressingly familiar: use-after-free, double-free, buffer overflows, null pointer dereferences, data races. Decades of tooling, sanitizers, and coding standards have reduced them, but not eliminated them, because the language itself does not forbid them. Correctness rests on the programmer never slipping up, across millions of lines and many years. That is not a great foundation to build a browser on.
Garbage-collected languages such as Python, Go, Java, and JavaScript take a different route. A runtime tracks which memory is still reachable and frees the rest for you. This genuinely solves memory safety, but it introduces a runtime cost: the collector needs CPU and memory, and it can pause your program at moments you do not control. For a web backend that is usually fine. For an audio engine, a game loop, a trading system, or a tiny microcontroller, an unpredictable pause is not acceptable.
Rust takes a third path. It gives you compile-time guarantees of memory and thread safety with no garbage collector and no runtime overhead. A component of the compiler called the borrow checker proves that your program cannot commit those classic mistakes. If the proof fails, the code does not compile. If it succeeds, whole categories of bugs are simply gone.
The mental shift is this: Rust moves bug discovery from runtime to compile time. In C you find the use-after-free when production crashes at 3am. In Rust you find it while you are still typing, as a red squiggle and a clear error message. That trade, pain now instead of pain later, is the whole deal.
The ownership model: Rust's key idea
Ownership is the single concept that makes everything else work. It is three rules the compiler enforces mechanically:
- Every value has an owner (a variable that is responsible for it).
- There is only one owner at a time.
- When the owner goes out of scope, the value is dropped and its memory is freed.
Those three rules quietly eliminate a lot of pain. There is no use-after-free, because you cannot use a value after its owner is gone. There is no double-free, because a single owner means a single cleanup. There is no null, because Rust does not have null at all; where other languages return null, Rust returns an Option<T> that the compiler forces you to handle.
Here is ownership moving in front of your eyes:
fn main() {
let s1 = String::from("hello");
let s2 = s1; // ownership MOVES from s1 into s2
// println!("{s1}"); // uncomment this and it will NOT compile
println!("{s2}"); // this is fine
}
In Python or Java, s2 = s1 would give you two names for the same object. In Rust it is a move: s1 hands the string over to s2 and is itself no longer usable. Why be so strict? Because a String owns a heap allocation. If both s1 and s2 were valid, both would try to free that allocation when they go out of scope, and that is a double-free. Rust removes the possibility by letting only one of them own it.
If moving away every value were the whole story, Rust would be exhausting to use. So there is borrowing, which lets you access a value without taking ownership of it:
fn char_count(s: &String) -> usize {
s.chars().count() // we only read s; we do not own it
}
fn main() {
let text = String::from("hello");
let n = char_count(&text); // lend text to the function
println!("{text} has {n} chars"); // text is still ours
}
The & creates a reference: a pointer that borrows the data but does not own it. When the reference goes away, nothing is freed. And borrowing has one rule worth tattooing on your brain: you may have any number of shared references (&T) at once, OR exactly one mutable reference (&mut T), but never both at the same time. Many readers or one writer, never both. Those are exactly the conditions that make a data race impossible, and the compiler checks them for you. You cannot write a data race in safe Rust even if you try.
Installing the toolchain
Enough theory. Let us get Rust on your machine. The official installer is rustup, which manages the compiler, the package manager, and your toolchain versions. On macOS or Linux, run this in a terminal:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
On Windows, download and run rustup-init.exe from https://rustup.rs instead. Accept the default installation when it asks. When it finishes, either restart your terminal or run source "$HOME/.cargo/env", then confirm the pieces are present:
rustc --version # the compiler
cargo --version # the build tool and package manager
If both print a version number, you are ready. Later, rustup update upgrades you to the newest stable Rust whenever you want it, which the Rust team ships on a predictable six-week cadence.
Your first program with cargo
You almost never call the rustc compiler by hand. You use cargo, which is build tool, package manager, test runner, and documentation generator all in one. Create a new project:
cargo new hello_rust
cd hello_rust
Cargo scaffolds a small project for you, and src/main.rs already contains a working hello world:
fn main() {
println!("Hello, world!");
}
fn main() is the entry point of every executable. println! is a macro, not a function, which is what the ! tells you; we will meet macros properly much later, but for now read it as "print a line". Build and run the whole thing with one command:
cargo run
You will see cargo compile the project and then print Hello, world!. Congratulations, you are officially a Rust programmer. ;-)
Anatomy of a Cargo project
Take a second to look at what cargo new actually created, because you will live in this structure for the rest of the series. There are two files and, after your first build, two extra things that matter.
The first is Cargo.toml, the manifest that describes your project and its dependencies:
[package]
name = "hello_rust"
version = "0.1.0"
edition = "2021"
[dependencies]
The [package] section names your project and sets its version and the language edition (editions are Rust's way of introducing small syntax changes every few years without breaking old code). The [dependencies] section is where you list external libraries, called crates, that you want to pull in from crates.io, Rust's package registry. Right now it is empty because hello world needs nothing but the standard library.
The second file is src/main.rs, your actual code. By convention main.rs produces an executable, while a file named src/lib.rs would produce a library other projects can depend on.
After your first cargo build or cargo run, two more things appear. A target/ directory holds all the compiled output (you never edit it, and you add it to .gitignore). And a Cargo.lock file records the exact versions of every dependency that was resolved, so that your build is reproducible on another machine or six months from now. For an application you commit Cargo.lock; for a library you usually do not. That is the whole project skeleton, and it is refreshingly boring, which is exactly what you want from build tooling.
The compiler as a teacher
Now let us deliberately break something, because Rust's error messages are one of the best reasons to use it. Change src/main.rs to reuse a moved value:
fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("{s1}"); // ERROR: s1 was moved into s2 on the line above
}
Run cargo run and the compiler refuses, but look at how it refuses:
error[E0382]: borrow of moved value: `s1`
--> src/main.rs:4:15
|
2 | let s1 = String::from("hello");
| -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 | let s2 = s1;
| -- value moved here
4 | println!("{s1}");
| ^^^^ value borrowed here after move
This is not a cryptic "segfault" you get to debug in a coredump. The compiler tells you the value moved on line 3, that line 4 tried to use it afterward, and even why the move happened (String is not Copy). Every one of these errors is a small lesson about ownership. For your first weeks in Rust you will read a lot of them, and each one is teaching you a pattern that would have been a latent bug in C. Occassionally you will fight an error for ten minutes and then realise the compiler was right the whole time, and you were about to write a real bug.
build, check, run, and release
A few cargo commands will cover almost everything early on:
cargo check # type-check fast, without producing a binary
cargo build # compile a debug binary into target/debug/
cargo run # build (if needed) and then run
cargo build --release # optimized binary into target/release/
cargo check is your fast feedback loop while you fix compiler errors, because it skips code generation entirely. Debug builds compile quickly and include extra checks that help you catch mistakes, such as panicking on integer overflow. When you actually want speed, --release turns on optimizations and can make numeric code many times faster, at the cost of slower compilation. A common beginner mistake is to benchmark a debug build, conclude Rust is slow, and never try --release. Do not be that person.
Zero-cost abstractions, briefly
Rust's other big promise is zero-cost abstractions: expressive, high-level code that compiles down to something as tight as if you had written the low-level loop by hand. Consider summing the doubled even numbers of a list:
fn main() {
let sum: i32 = (1..=5)
.map(|x| x * 2)
.filter(|x| *x > 5)
.sum();
println!("{sum}"); // 24
}
That reads like functional programming with iterators, closures, and adapters. The compiler turns it into a single loop with no intermediate collections and no heap allocation. You get the readable version and the fast version at the same time. A garbage-collected language cannot fully match this, because the collector has a cost that never goes to zero, and matching it in C means writing the low-level loop yourself and hoping you got the bounds right.
When Rust is the right tool, and when it is not
Rust is the right tool when performance is non-negotiable (engines, databases, cryptography), when GC pauses are unacceptable (audio, real-time control, embedded), when memory safety is critical (browsers, network services facing untrusted input), or when you are replacing fragile C or C++. In those places Rust genuinely has no equal right now: it is as fast as C and vastly harder to write a security hole in.
Rust is the wrong tool when you are prototyping fast and correctness can wait, when GC overhead is completely fine (most CRUD web backends), or when the mature ecosystem lives elsewhere (data science and machine learning in Python, frontend in JavaScript). Picking the right tool is a seperate skill from knowing a language, and "always Rust" is not the answer. The honest answer is that Rust shines exactly where the classic systems languages hurt, and it is overkill where a scripting language would have shipped the feature yesterday.
How Rust compares to what you already know
If you are coming from C or C++, Rust will feel familiar in spirit and alien in the details. You get the same low-level control, the same direct path to fast machine code, the same absence of a runtime babysitting you. What changes is that the compiler now refuses the memory bugs you used to catch in code review, if you were lucky. The cost is the borrow checker and longer compile times; the payoff is that "it compiles" starts to mean a great deal more than it ever did in C++.
If you are coming from Go, the trade is almost the mirror image. Go optimises for simplicity and fast compilation and accepts a garbage collector; Rust optimises for control and safety and accepts complexity. Go is a joy for a network service where developer speed matters more than the last ten percent of performance. Rust wins where that ten percent, or a hard latency ceiling, is the whole point. Both are modern, both have excellent tooling, and plenty of teams reach for one where the other would also have worked fine.
If you are coming from Python or JavaScript, the biggest adjustment is not syntax, it is that the types are static and the memory is yours to reason about. You give up the ability to prototype something in five lines without thinking, and you gain performance in a completely different league plus the confidence that a refactor either compiles or tells you exactly what broke. A useful rule of thumb: keep writing the quick script in Python, and reach for Rust when that script grows into a service that has to be fast, correct, and running unattended for months.
The point of all these comparisons is not that Rust is "better" in the abstract. It is that Rust occupies a specific niche, safe systems programming with no runtime cost, and inside that niche it is very hard to beat. Knowing where the niche ends is as valuable as knowing the language itself.
What tends to surprise newcomers
A few things reliably catch people coming from other languages, so let me set expectations now. First, the borrow checker will reject code that looks obviously fine to you, and you will restructure your data flow more than once before it clicks. This difficulty is front-loaded: the first weeks are a fight, and then ownership becomes intuition, and code the compiler would reject starts looking wrong to you before you even hit compile.
Second, compile times are longer than you are used to, especially on a big project with many dependencies. cargo check softens this a lot during development, but it is a real trade you make for the guarantees.
Third, there is often exactly one idiomatic way to do a thing, and the tooling nudges you toward it hard. cargo fmt formats your code, cargo clippy lints it with genuinely helpful suggestions, and the community leans on both. After a while this uniformity feels like a feature, not a straitjacket, because every Rust codebase reads the same way.
Try it yourself
Before the next episode, do this small exercise to make the ideas concrete:
- Run
cargo new ownership_playand opensrc/main.rs. - Make a
String, move it into a second variable, and try to print the first one. Read the error the compiler gives you and notice how specific it is. - Now fix it two different ways: once by borrowing (
&) instead of moving, and once by cloning (let s2 = s1.clone();). Notice that cloning copies the heap data so both are valid, while borrowing avoids the copy entirely.
Sit with the difference between move, borrow, and clone. That triangle is the heart of Rust, and getting comfortable with it now makes everything ahead easier.
So what did we actually cover?
- Rust solves memory safety without a garbage collector by proving safety at compile time with the borrow checker.
- Ownership (one owner, dropped at end of scope) eliminates use-after-free, double-free, and null.
- Borrowing lets you use data without owning it, and the shared-xor-mutable rule makes data races impossible in safe Rust.
- rustup installs the toolchain, and cargo builds, runs, checks, and packages your code, with
Cargo.toml,target/, andCargo.lockforming a simple, predictable project. - You read a real compiler error and understood it, which is a skill you will use every single day in Rust.
- Zero-cost abstractions give you readable code and low-level speed together, and Rust is worth it exactly where performance, predictability, or safety are non-negotiable.
Rust asks you to think about ownership, borrowing, and lifetimes up front. That is real work. In return it hands you guarantees that no other mainstream systems language offers. Next time we start writing actual code in earnest, beginning with variables, types, and functions.
Congratulations @scipio! You have completed the following achievement on the Hive blockchain And have been rewarded with New badge(s)
Your next target is to reach 500 posts.
You can view your badges on your board and compare yourself to others in the Ranking
If you no longer want to receive notifications, reply to this comment with the word
STOPis difficult learn rust ?