DS DevShelfHub Projects · AI tools
Cheatsheets / Rust
Cheatsheet · Languages

Rust: Ownership, Traits and Cargo Reference Guide

By DevShelfHub

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 2021 async 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

Where things liveStandard library map

std::collections (Vec, HashMap, BTreeMap, VecDeque, HashSet)Core containers.
std::io (Read, Write, BufReader, BufWriter)Streaming I/O traits + buffers.
std::fsFiles and directories.
std::envCLI args, env vars, current dir.
std::processSpawn child processes, exit codes.
std::thread, std::sync, std::sync::mpscOS threads, Arc / Mutex / RwLock, channels.
std::timeDuration, Instant, SystemTime.
std::fmt::{Debug, Display}Two distinct print traits. Derive Debug; impl Display.
std::error::ErrorMarker 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 / isizeSigned integers. isize is pointer-sized.
u8 / u16 / u32 / u64 / u128 / usizeUnsigned. usize for indexing.
f32 / f64IEEE-754.
bool, charchar is a Unicode scalar (4 bytes).
str, StringBorrowed 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 runsDeterministic cleanup. Files, sockets, locks — all RAII.

References

&TShared (immutable) reference. Many allowed.
&mut TExclusive (mutable) reference. Only one at a time.
Aliasing XOR mutabilityThe 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 strTie outputs to inputs. 'a is a name.
'staticLives for the whole program. String literals are &'static str.
Elision rulesCompiler 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}");
}

Data types · pattern matchStructs, enums, match

Structs

struct Point { x: f64, y: f64 }Named-field struct.
struct Wrapper(i32);Tuple struct. Use for newtype pattern.
struct Unit;Unit struct. Often used as a marker.
impl Point { fn new(…) -> Self {…} }Inherent methods. Self = the type.
#[derive(Debug, Clone, PartialEq)]Derive common traits.
let p = Point { x: 1.0, y: 2.0 };Struct literal.
let p2 = Point { y: 9.0, ..p };Functional update.

Enums

enum Shape { Circle(f64), Rect { w: f64, h: f64 } }Tagged union. Variants can carry data.
Option<T> = Some(T) | NoneNullable replacement. The compiler enforces handling.
Result<T, E> = Ok(T) | Err(E)Recoverable errors.

Pattern matching

match x { Some(v) => v, None => 0 }Exhaustive. Compiler errors on missing arms.
match n { 1 | 2 => …, 3..=5 => …, _ => … }Or-patterns and ranges.
if let Some(v) = opt { … }Single-pattern alternative to match.
while let Some(x) = it.next() { … }Loop until pattern fails.
let Some(v) = opt else { return; };Early-exit binding.
Guards: x if x > 0 => …Conditional arms.

PolymorphismTraits & generics

trait Greet { fn hello(&self) -> String; }Define an interface. Methods + associated types + consts.
impl Greet for Person { … }Implement for any type you own (or a foreign type for your trait).
fn shout<T: Greet>(g: &T) -> StringGeneric with trait bound. Monomorphized at compile time.
fn shout(g: &impl Greet)Sugar for the same thing.
fn shout(g: &dyn Greet)Trait object. Runtime polymorphism via vtable.
where T: Display + Clone + 'staticMulti-bound clauses. Lifetimes too.
type Item; (associated type)Type-level slot. Iterator::Item is the classic example.
Default impls inside traitImplementors override only what they need.
Blanket impls: impl<T: Display> ToString for TImplement for "all types satisfying X".

Worked example

rust
use std::fmt::Display;

// A trait = interface. Methods can have default impls.
trait Greet {
    fn name(&self) -> &str;
    fn hello(&self) -> String { format!("hi {}", self.name()) }
}

struct Person { full_name: String }

impl Greet for Person {
    fn name(&self) -> &str { &self.full_name }
}

// Generic function — bounded by trait
fn shout(g: &T) -> String { g.hello().to_uppercase() }

// where-clause for multiple bounds
fn label(x: T) -> String where T: Display + Clone {
    format!("{} (cloned: {})", x.clone(), x)
}

// Trait objects = runtime polymorphism (dyn)
fn greet_many(gs: &[Box]) {
    for g in gs { println!("{}", g.hello()); }
}

fn main() {
    let p = Person { full_name: "Ada".into() };
    println!("{}", shout(&p));
    println!("{}", label(42));

    let people: Vec> = vec![
        Box::new(Person { full_name: "Ada".into() }),
        Box::new(Person { full_name: "Grace".into() }),
    ];
    greet_many(&people);
}

Result · ? · OptionErrors

fn f() -> Result<T, E>Return success or failure.
let v = f()?;Preferred Propagate errors. Returns early on Err.
map / and_then / or_elseCombinators on Result + Option.
.ok() / .err()Result → Option of the success / failure side.
.unwrap() / .expect("msg")Panic on Err / None. Use in tests, not prod paths.
.unwrap_or(default), .unwrap_or_else(fn)Safe extraction with fallback.
thiserror::Error (lib crates)Derive structured error enums.
anyhow::Result<T> (app crates)Single boxed error type. Context via .context(…).
panic! / unreachable!Unrecoverable abort. Don’t use as control flow.

Worked example

rust
use std::fs;
use std::num::ParseIntError;
use thiserror::Error;

// Custom error enum — variants for each failure source
#[derive(Debug, Error)]
enum LoadError {
    #[error("io error: {0}")]
    Io(#[from] std::io::Error),
    #[error("parse error: {0}")]
    Parse(#[from] ParseIntError),
}

// ? propagates errors after From conversion
fn load_count(path: &str) -> Result {
    let contents = fs::read_to_string(path)?;     // io::Error → LoadError
    let n: u32 = contents.trim().parse()?;        // ParseIntError → LoadError
    Ok(n)
}

fn main() {
    match load_count("count.txt") {
        Ok(n)  => println!("count = {n}"),
        Err(e) => eprintln!("failed: {e}"),
    }

    // Option chaining — None short-circuits
    let first_word: Option<&str> = "hello world"
        .split_whitespace()
        .next();
    let upper = first_word.map(str::to_uppercase).unwrap_or_default();
    println!("{upper}");
}

Lazy · chainableIterators

v.iter() / .iter_mut() / .into_iter()Borrow / borrow-mut / consume the container.
.map(|x| …).filter(|x| …).collect()The classic pipeline.
.collect::<Vec<_>>()Turbofish to disambiguate the target type.
.fold(init, |acc, x| …)Reduce with explicit accumulator.
.sum() / .product() / .count()Common terminal reductions.
.zip(other) / .enumerate() / .chain(other)Combine streams.
.take(n) / .skip(n) / .step_by(n)Slicing operations.
.flat_map(fn) / .flatten()Flatten nested iterators.
for x in &vSugar for v.iter().
Iterators are lazyNothing happens until collect / for / a terminal op.

Worked example

rust
fn main() {
    let scores = vec![88, 42, 95, 71, 63];

    // Lazy pipeline — nothing runs until a terminal op
    let top: Vec<_> = scores
        .iter()
        .copied()
        .filter(|n| *n >= 70)
        .map(|n| n + 5)
        .collect();
    println!("boosted passing: {top:?}");

    // fold for custom reductions
    let total: i32 = scores.iter().copied().fold(0, |acc, n| acc + n);
    println!("sum: {total}");

    // zip + enumerate for paired iteration
    let names = ["ada", "linus", "grace"];
    for (i, (name, score)) in names.iter().zip(scores.iter()).enumerate() {
        println!("{i}: {name} scored {score}");
    }

    // Option/Result combinators chain cleanly
    let parsed: i32 = "42"
        .parse()
        .map(|n: i32| n * 2)
        .unwrap_or(0);
    println!("parsed: {parsed}");
}

Heap · shared · interior mutabilitySmart pointers

Box<T>Heap-allocate a T. Unique ownership.
Rc<T>Reference-counted shared ownership, single-threaded.
Arc<T>Atomic Rc — thread-safe. Cheap clone of a handle, not the data.
RefCell<T>Interior mutability, runtime-checked. Single-threaded.
Mutex<T> / RwLock<T>Thread-safe interior mutability. Use with Arc.
Cell<T>Move-based interior mutability for Copy types.
Cow<'a, T>Clone-on-write. Borrow until you have to mutate.
Weak<T>Non-owning handle to break Rc / Arc cycles.

Futures · tokioAsync

async fn f() -> TReturns impl Future<Output = T>.
let v = f().await;Suspend until ready.
#[tokio::main]Macro that wires up the runtime around main.
tokio::spawn(async { … })Spawn a task on the executor.
tokio::time::sleep(Duration::…).awaitNon-blocking sleep.
tokio::select! { _ = a => …, v = b => … }Race multiple futures.
futures::join!(a, b, c)Wait for all in parallel.
tokio::sync::Mutex / mpsc::channelAsync-aware sync primitives. Different from std versions.
async-trait crateWorkaround until async fn in traits stabilizes object safety.

ToolchainCargo

cargo new / cargo initScaffold a new / existing dir.
cargo build / build --releaseDebug vs optimized.
cargo checkType-check only. Fast.
cargo run -- arg1 arg2Args after --.
cargo add foo --features bar,bazAdd a dep with feature flags.
cargo remove fooRemove a dep.
cargo test --lib -- test_nameFilter by test name.
cargo doc --openBuild + open docs for your crate + deps.
cargo fmt && cargo clippyFormat + lint. Run before commit.
[workspace] in root Cargo.tomlMulti-crate repo.
[features] default = ["a"]Optional compile-time switches.
[profile.release] lto = "thin"Per-profile compiler flags.

Async HTTP fan-out · ~30 linesEnd-to-end · Async HTTP

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.

Go deeperSee also

Rust FAQ

What is Rust ownership?

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.