Ownership, borrowing, lifetimes, traits, enums, pattern matching, Result/Option, iterators, async/await, Cargo — the Rust 2021 stable surface for systems and backend developers.
122 items
◷ 9 min
Ownership
Traits
Result
Start hereQuick start · 6 you’ll reach for daily
New projectcargo new myapp
Add depcargo add tokio --features full
Runcargo run --release
Borrowfn f(s: &str) / &mut T
Propagate errlet v = f()?
Pattern matchmatch x { Some(v) => … }
What this page coversOverview
Rust is a systems language from Mozilla Research (1.0 in 2015) built around a single idea: you can have
C-level performance without garbage collection or manual free.
The compiler enforces memory safety through ownership (every value
has one owner), borrowing (temporary references with compile-time
aliasing rules), and lifetimes (how long those references stay valid).
That model catches use-after-free, data races, and null dereferences before your binary ships — which
is why Rust shows up in kernels, browsers, blockchains, and security-sensitive services where a single
memory bug is unacceptable.
At the API surface, Rust feels like a modern imperative language with algebraic data types. You model state
with struct and enum,
express absence with Option<T>, and propagate failures with
Result<T, E> and the ?
operator. Polymorphism comes from traits (static dispatch via generics,
dynamic dispatch via dyn Trait) and zero-cost abstractions like
iterators. Async Rust (stable since 1.39, maturing through 1.75+) compiles async fn
bodies into state machines; you still pick a runtime — usually tokio
— because the standard library ships no executor. cargo ties it
together: dependency resolution, builds, tests, docs, and publishing to crates.io from one manifest.
This cheatsheet targets the Rust 2021 edition on stable
(rustc ≥ 1.75). It is a field guide for developers who already
write code in another language and need the everyday syntax, stdlib map, and idioms — not a full tutorial.
Pair it with The Book for
narrative depth, or jump straight to the ownership and error-handling sections when the borrow checker blocks you.
Target versions · paceVersions
Targets:rustc ≥ 1.75 (stable)edition 2021async fn in traits (1.75)let-else (1.65)
Pin the toolchain per project with a rust-toolchain.toml. Editions are
opt-in language flavors — 2021 is the current default; 2024 is on the horizon. Async in stable Rust still has
rough edges (no built-in executor — pick tokio or
async-std). For a long-lived codebase, also pin clippy
and rustfmt — their suggestions evolve.
Install · CargoSetup
bash
# Install via rustup — the official toolchain manager
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Pin / switch toolchains
rustup default stable
rustup toolchain install nightly
rustup component add rustfmt clippy rust-analyzer
# Project setup
cargo new myapp # binary crate; add --lib for a library
cd myapp && cargo run # build + run
cargo add tokio --features full # add a dependency (cargo-edit, bundled)
cargo build --release # optimized binary in target/release/
# Daily commands
cargo check # type-check without producing a binary
cargo test # run tests
cargo fmt && cargo clippy -- -D warnings
cargo bench # nightly only; or use criterion on stable
Two distinct print traits. Derive Debug; impl Display.
std::error::Error
Marker trait. Most ecosystems use anyhow or thiserror.
use crate::… / use super::… / use self::…
Module path keywords.
pub use foo::Bar;
Re-export. Curate your crate’s public API.
Bindings · primitivesVariables & types
Bindings
let x = 5;
Immutable by default.
let mut y = 5;
Mutable. Compiler warns if never mutated.
const PI: f64 = 3.14;
Compile-time constant. Type required.
static GREETING: &str = "hi";
Single instance, 'static lifetime.
let x: u32 = 5;
Type annotation.
let (a, b) = (1, 2);
Destructuring bind.
let Some(v) = opt else { return; };
Preferred let-else (1.65+) for early exit.
Primitive types
i8 / i16 / i32 / i64 / i128 / isize
Signed integers. isize is pointer-sized.
u8 / u16 / u32 / u64 / u128 / usize
Unsigned. usize for indexing.
f32 / f64
IEEE-754.
bool, char
char is a Unicode scalar (4 bytes).
str, String
Borrowed UTF-8 slice vs owned heap string.
[T; N]
Array, fixed size.
&[T] / Vec<T>
Borrowed slice / owned dynamic array.
(T, U, V)
Tuple. Heterogeneous.
()
Unit type. The "no value" type, like void.
The core ideaOwnership, borrowing, lifetimes
Ownership
let s = String::from("hi"); let t = s;
Move.s is invalidated.
let t = s.clone();
Explicit deep copy. Both usable.
Copy types (i32, bool, &T, …)
Bit-copied on assign, no move semantics.
When a value goes out of scope, Drop runs
Deterministic cleanup. Files, sockets, locks — all RAII.
References
&T
Shared (immutable) reference. Many allowed.
&mut T
Exclusive (mutable) reference. Only one at a time.
Aliasing XOR mutability
The whole rule, in 3 words.
fn f(s: &str)
Borrow. Avoid passing String when &str works.
&v[i..j]
Slice. View into owned data, no copy.
Lifetimes
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str
Tie outputs to inputs. 'a is a name.
'static
Lives for the whole program. String literals are &'static str.
Elision rules
Compiler infers many lifetimes. Annotate only when it can’t.
struct Foo<'a> { name: &'a str }
Lifetime parameter on a type.
Worked example
rust
fn main() {
// Move — owner changes, original is invalidated
let s = String::from("hi");
let t = s; // s is moved into t
// println!("{s}"); // ERROR: borrow of moved value
// Clone — explicit deep copy when you want both
let a = String::from("hi");
let b = a.clone();
println!("{a} {b}");
// Borrow — &T is a shared reference, &mut T is exclusive
let mut v = vec![1, 2, 3];
let r = &v; // many shared refs OK
let m = &mut v; // exclusive — no other refs may exist now
m.push(4);
println!("{m:?}"); // [1, 2, 3, 4]
// Slices borrow a contiguous view
let nums = vec![10, 20, 30, 40];
let head = &nums[..2]; // &[i32], no copy
println!("{head:?}");
// Strings: String is owned, &str is borrowed
let owned: String = String::from("DevShelf");
let borrowed: &str = &owned; // implicit deref coercion
print_label(borrowed);
}
fn print_label(s: &str) { // takes any &str — cheap, no allocation
println!("label: {s}");
}
Tokio + reqwest + serde + anyhow. Fan out concurrent GitHub API requests, parse JSON, collect results — canonical async Rust shape.
rust
// Cargo.toml:
// [dependencies]
// tokio = { version = "1", features = ["full"] }
// reqwest = { version = "0.12", features = ["json"] }
// serde = { version = "1", features = ["derive"] }
// anyhow = "1"
// futures = "0.3"
use anyhow::Result;
use futures::future::join_all;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct Repo { name: String, stargazers_count: u32 }
async fn fetch(client: &reqwest::Client, owner: &str) -> Result> {
let url = format!("https://api.github.com/users/{owner}/repos?per_page=5");
let repos = client.get(url)
.header("User-Agent", "devshelf")
.send().await?
.error_for_status()?
.json::>().await?;
Ok(repos)
}
#[tokio::main]
async fn main() -> Result<()> {
let client = reqwest::Client::new();
let owners = ["rust-lang", "tokio-rs", "serde-rs"];
// Fan out concurrently, join, collect
let results = join_all(
owners.iter().map(|o| fetch(&client, o))
).await;
for (owner, res) in owners.iter().zip(results) {
match res {
Ok(repos) => for r in repos {
println!("{owner}/{} ★{}", r.name, r.stargazers_count);
},
Err(e) => eprintln!("{owner}: {e}"),
}
}
Ok(())
}
Best practiceGood to know
Lean on &str over String in signatures.&str accepts both String (via deref) and string literals — you stay flexible and avoid unnecessary allocations.
Use thiserror for library errors, anyhow for applications.
Libraries should expose typed errors so callers can match on them; binaries usually just want context-chained "explain what went wrong".
cargo clippy teaches you idiomatic Rust.
Treat its warnings as a tutor, not a nag. Almost every lint has a "why" link to the project’s wiki.
Prefer into_iter() when you own the collection.
It moves elements out without cloning. Use iter() when you only need to read;
iter_mut() when you need in-place mutation.
Pin dependency versions in libraries, not just apps.
Library crates should specify compatible semver ranges in Cargo.toml;
binaries can use cargo update -p foo --precise 1.2.3 to lock a transitive dep
when a regression lands upstream.
Common trapsWatch out for
Indexing returns by value, not reference.v[0] on a Vec<String> tries to move out of the vec — usually you want &v[0] or v.get(0).
Async cancellation drops futures mid-execution.
A select! arm losing the race aborts its future at the next .await. Anything not idempotent (DB write, file flush) needs cancellation-safe primitives.
Arc<Mutex<T>> isn’t free parallelism.
It serializes access. Heavy contention can be slower than single-threaded. Reach for RwLock, message passing, or sharding before more locks.
Mixing std::sync and tokio::sync primitives blocks the executor.
Holding a std::sync::Mutex guard across an .await can deadlock
the runtime. Use tokio::sync::Mutex inside async code, or keep critical sections sync-only.
#[derive(Clone)] on large structs is expensive.
Deriving Clone recursively copies every field. For big buffers, wrap in
Arc or implement a shallow clone manually.
Rust ownership is a compile-time memory model where every value has exactly one owner. When the owner goes out of scope, the value is dropped automatically — no garbage collector and no manual free.
What is borrowing in Rust?
Borrowing lets you reference a value without taking ownership. Immutable references (&T) allow many readers; &mut T allows one writer. The borrow checker enforces these rules at compile time.
When should I use Result vs Option in Rust?
Use Option when a value may be absent — optional config, safe indexing. Use Result when an operation can fail with a reason — file I/O, network calls, or parsing user input.
What is Cargo in Rust?
Cargo is Rust's build tool and package manager. cargo new creates projects, cargo add pulls dependencies from crates.io, and cargo build, run, and test drive the everyday dev loop.
Is Rust hard to learn for beginners?
Rust has a steep initial curve around ownership and lifetimes, but the compiler teaches you as you go. Developers from C, C++, or Go often pick it up faster; Python or JavaScript devs should budget extra time for the type system.
What are Rust traits?
Traits define shared behavior — like interfaces or type classes. They power generics, operator overloading, and async functions. Most std traits (Debug, Clone, Iterator) are derived or implemented on your types.
What is the Rust borrow checker?
The borrow checker is the compile-time analysis that enforces Rust's ownership rules: either many shared references or one mutable reference to a value at a time, and references cannot outlive their data. It prevents data races and use-after-free without a runtime cost.
Should I use tokio or async-std for async Rust?
Tokio is the de facto standard — largest ecosystem, most crates assume it, and it powers production services at scale. async-std is a smaller alternative with a familiar API. For new projects, default to tokio unless you have a specific reason not to.