Intermediate
Want to build production-ready Rust applications? This tutorial covers advanced language features needed for real-world Rust development.
Coverage
This tutorial covers 60-85% of Rust knowledge - production-grade features and patterns.
Prerequisites
- Beginner Tutorial complete
- Strong understanding of ownership, borrowing, and lifetimes
- Familiarity with structs, enums, and pattern matching
- Comfortable with Result and error handling
Learning Outcomes
By the end of this tutorial, you will:
- Write generic code with type parameters and trait bounds
- Design and implement traits for custom types
- Master lifetime annotations for complex borrowing scenarios
- Use smart pointers (Box, Rc, Arc, RefCell) effectively
- Build concurrent programs with threads and channels
- Write async code with async/await and Tokio
- Leverage iterators and closures for functional patterns
- Implement production error handling strategies
- Organize and test production-grade Rust projects
Learning Path
graph TD
A[Generics] --> B[Traits]
B --> C[Lifetimes ⭐]
C --> D[Smart Pointers]
D --> E[Concurrency ⭐]
E --> F[Async/Await ⭐]
F --> G[Iterators & Closures]
G --> H[Error Handling Patterns]
H --> I[Testing Strategies]
style C fill:#DE8F05,stroke:#000000,stroke-width:3px,color:#000000
style E fill:#DE8F05,stroke:#000000,stroke-width:3px,color:#000000
style F fill:#DE8F05,stroke:#000000,stroke-width:3px,color:#000000
Color Palette: Orange (#DE8F05 - critical sections for production Rust)
Section 1: Generics
Generics allow you to write code that works with multiple types.
Generic Functions
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("The largest number is {}", result);
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest(&char_list);
println!("The largest char is {}", result);
}<T: PartialOrd> is a trait bound - T must implement PartialOrd (comparison).
Generic Structs
struct Point<T> {
x: T,
y: T,
}
fn main() {
let integer = Point { x: 5, y: 10 };
let float = Point { x: 1.0, y: 4.0 };
}Multiple type parameters:
struct Point<T, U> {
x: T,
y: U,
}
fn main() {
let both_integer = Point { x: 5, y: 10 };
let both_float = Point { x: 1.0, y: 4.0 };
let integer_and_float = Point { x: 5, y: 4.0 };
}Generic Enums
enum Option<T> {
Some(T),
None,
}
enum Result<T, E> {
Ok(T),
Err(E),
}Generic Methods
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
impl Point<f32> {
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
fn main() {
let p = Point { x: 5, y: 10 };
println!("p.x = {}", p.x());
let p2 = Point { x: 3.0, y: 4.0 };
println!("Distance from origin: {}", p2.distance_from_origin());
}Monomorphization
Rust generates specialized code for each concrete type used:
let integer = Some(5);
let float = Some(5.0);Compiles to equivalent of:
enum Option_i32 {
Some(i32),
None,
}
enum Option_f64 {
Some(f64),
None,
}Zero-cost abstraction: Generics have no runtime overhead.
Section 2: Traits
Traits define shared behavior - similar to interfaces in other languages.
Defining Traits
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}Default Implementations
pub trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}
impl Summary for Tweet {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
// Uses default summarize implementation
}Traits as Parameters
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}Trait bound syntax:
pub fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}Multiple trait bounds:
pub fn notify(item: &(impl Summary + Display)) {
// ...
}
pub fn notify<T: Summary + Display>(item: &T) {
// ...
}where clauses for complex bounds:
fn some_function<T, U>(t: &T, u: &U) -> i32
where
T: Display + Clone,
U: Clone + Debug,
{
// ...
}Returning Types that Implement Traits
fn returns_summarizable() -> impl Summary {
Tweet {
username: String::from("horse_ebooks"),
content: String::from("of course, as you probably already know, people"),
reply: false,
retweet: false,
}
}Limitation: Can only return a single concrete type:
fn returns_summarizable(switch: bool) -> impl Summary {
if switch {
NewsArticle { /* ... */ } // ❌ Error
} else {
Tweet { /* ... */ } // Different type
}
}Solution: Use trait objects (covered in Advanced tutorial).
Implementing Traits on Types
use std::fmt;
struct Point {
x: i32,
y: i32,
}
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
fn main() {
let origin = Point { x: 0, y: 0 };
println!("Origin: {}", origin); // Uses Display implementation
}Trait Bounds with Generic Implementations
use std::fmt::Display;
struct Pair<T> {
x: T,
y: T,
}
impl<T> Pair<T> {
fn new(x: T, y: T) -> Self {
Self { x, y }
}
}
impl<T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
if self.x >= self.y {
println!("The largest member is x = {}", self.x);
} else {
println!("The largest member is y = {}", self.y);
}
}
}Only Pair<T> where T implements both Display and PartialOrd gets the cmp_display method.
Section 3: Lifetimes
Lifetime annotations describe how long references are valid.
Preventing Dangling References
fn main() {
let r;
{
let x = 5;
r = &x; // ❌ Error: `x` does not live long enough
}
println!("r: {}", r);
}Rust’s borrow checker prevents this at compile time.
Lifetime Annotation Syntax
&i32 // a reference
&'a i32 // a reference with an explicit lifetime
&'a mut i32 // a mutable reference with an explicit lifetime
Lifetime parameters describe relationships between references’ lifetimes.
Lifetime Annotations in Functions
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
println!("The longest string is {}", result);
}
}'a means: The returned reference will be valid as long as both input references are valid.
Lifetime Elision Rules
Rust can infer lifetimes in many cases:
fn first_word(s: &str) -> &str { // Lifetimes inferred
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}Elision rules:
- Each reference parameter gets its own lifetime
- If exactly one input lifetime, output gets that lifetime
- If multiple input lifetimes and one is
&selfor&mut self, output getsself’s lifetime
Lifetime Annotations in Struct Definitions
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
let i = ImportantExcerpt {
part: first_sentence,
};
}ImportantExcerpt can’t outlive the reference it holds.
Lifetime Annotations in Method Definitions
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {}", announcement);
self.part
}
}The Static Lifetime
let s: &'static str = "I have a static lifetime.";'static means reference lives for entire program duration.
String literals have 'static lifetime (stored in program binary).
Be careful: Most lifetime problems shouldn’t be solved with 'static.
Lifetime Visualization
graph TD
A["'a: longest lifetime"] --> B["x: &'a str"]
A --> C["y: &'a str"]
A --> D["return: &'a str"]
E["Calling code"] --> F["string1 lives here"]
E --> G["string2 lives shorter"]
G -.->|constrains| A
F --> A
style A fill:#DE8F05,stroke:#000000,stroke-width:2px,color:#000000
style B fill:#0173B2,stroke:#000000,stroke-width:2px,color:#FFFFFF
style C fill:#0173B2,stroke:#000000,stroke-width:2px,color:#FFFFFF
style D fill:#029E73,stroke:#000000,stroke-width:2px,color:#FFFFFF
Section 4: Smart Pointers
Smart pointers are data structures that act like pointers but have additional metadata and capabilities.
Box - Heap Allocation
Use cases:
- When you have a type whose size can’t be known at compile time
- When you want to transfer ownership of a large amount of data without copying
- When you want a value that implements a specific trait (trait objects)
Basic usage:
fn main() {
let b = Box::new(5);
println!("b = {}", b);
}Enabling recursive types:
enum List {
Cons(i32, Box<List>),
Nil,
}
use List::{Cons, Nil};
fn main() {
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}Without Box, List would have infinite size.
Rc - Reference Counting
Use case: Multiple ownership of heap data
use std::rc::Rc;
enum List {
Cons(i32, Rc<List>),
Nil,
}
use List::{Cons, Nil};
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
println!("count after creating a = {}", Rc::strong_count(&a));
let b = Cons(3, Rc::clone(&a));
println!("count after creating b = {}", Rc::strong_count(&a));
{
let c = Cons(4, Rc::clone(&a));
println!("count after creating c = {}", Rc::strong_count(&a));
}
println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}Output:
count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2Note: Rc<T> is only for single-threaded scenarios. Use Arc<T> for multi-threaded.
RefCell - Interior Mutability
Interior mutability: Mutate data even when there are immutable references to that data.
use std::cell::RefCell;
fn main() {
let value = RefCell::new(5);
*value.borrow_mut() += 10; // Mutate through immutable RefCell
println!("value: {:?}", value);
}Borrowing rules enforced at runtime instead of compile time:
use std::cell::RefCell;
fn main() {
let value = RefCell::new(5);
let borrowed1 = value.borrow_mut();
let borrowed2 = value.borrow_mut(); // ❌ Panics at runtime
}Combining Rc
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
enum List {
Cons(Rc<RefCell<i32>>, Rc<List>),
Nil,
}
use List::{Cons, Nil};
fn main() {
let value = Rc::new(RefCell::new(5));
let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));
let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a));
let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a));
*value.borrow_mut() += 10;
println!("a after = {:?}", a);
println!("b after = {:?}", b);
println!("c after = {:?}", c);
}Arc - Atomic Reference Counting
For multi-threaded scenarios:
use std::sync::Arc;
use std::thread;
fn main() {
let counter = Arc::new(5);
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
println!("counter = {}", counter);
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
}Arc vs Rc:
Rc<T>: Single-threaded reference countingArc<T>: Atomic (thread-safe) reference counting (slightly slower)
Section 5: Concurrency
Rust’s ownership and type system prevent data races at compile time.
Creating Threads
use std::thread;
use std::time::Duration;
fn main() {
thread::spawn(|| {
for i in 1..10 {
println!("hi number {} from the spawned thread!", i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("hi number {} from the main thread!", i);
thread::sleep(Duration::from_millis(1));
}
}Waiting for Threads with join
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 1..10 {
println!("hi number {} from the spawned thread!", i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("hi number {} from the main thread!", i);
thread::sleep(Duration::from_millis(1));
}
handle.join().unwrap(); // Wait for spawned thread to finish
}Using move Closures with Threads
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("Here's a vector: {:?}", v);
});
handle.join().unwrap();
}move keyword forces closure to take ownership.
Message Passing with Channels
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("hi");
tx.send(val).unwrap();
});
let received = rx.recv().unwrap();
println!("Got: {}", received);
}mpsc: multiple producer, single consumer
Sending multiple values:
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("thread"),
];
for val in vals {
tx.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
});
for received in rx {
println!("Got: {}", received);
}
}Multiple producers:
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
let tx1 = tx.clone();
thread::spawn(move || {
let vals = vec![String::from("hi"), String::from("from"), String::from("the")];
for val in vals {
tx1.send(val).unwrap();
}
});
thread::spawn(move || {
let vals = vec![String::from("more"), String::from("messages")];
for val in vals {
tx.send(val).unwrap();
}
});
for received in rx {
println!("Got: {}", received);
}
}Shared-State Concurrency with Mutex
use std::sync::Mutex;
fn main() {
let m = Mutex::new(5);
{
let mut num = m.lock().unwrap();
*num = 6;
} // lock is released here
println!("m = {:?}", m);
}Sharing Mutex
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}Send and Sync Traits
Send: Ownership can be transferred between threads
Sync: Safe to reference from multiple threads
Almost all types are Send and Sync. Exceptions:
Rc<T>: NotSendorSyncRefCell<T>: NotSync
Manually implementing Send and Sync is unsafe.
Section 6: Async/Await
Asynchronous programming for I/O-bound tasks.
The Future Trait
trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
enum Poll<T> {
Ready(T),
Pending,
}Don’t implement manually - use async keyword.
async Functions
async fn hello_world() {
println!("Hello, world!");
}Returns a Future.
await Keyword
use std::time::Duration;
async fn learn_song() -> String {
tokio::time::sleep(Duration::from_secs(1)).await;
String::from("🎵 Rust song learned!")
}
async fn sing_song(song: String) {
println!("Singing: {}", song);
}
async fn dance() {
println!("💃 Dancing!");
}
async fn learn_and_sing() {
let song = learn_song().await;
sing_song(song).await;
}
async fn async_main() {
let f1 = learn_and_sing();
let f2 = dance();
tokio::join!(f1, f2);
}
#[tokio::main]
async fn main() {
async_main().await;
}Add to Cargo.toml:
[dependencies]
tokio = { version = "1", features = ["full"] }Tokio Runtime
#[tokio::main]
async fn main() {
println!("Hello from Tokio!");
}#[tokio::main] macro sets up async runtime.
Spawning Async Tasks
use tokio::task;
#[tokio::main]
async fn main() {
let handle = task::spawn(async {
// Async work here
"result from spawned task"
});
let result = handle.await.unwrap();
println!("Got: {}", result);
}Async Channels
use tokio::sync::mpsc;
#[tokio::main]
async fn main() {
let (tx, mut rx) = mpsc::channel(32);
tokio::spawn(async move {
tx.send("hello").await.unwrap();
});
if let Some(message) = rx.recv().await {
println!("Got: {}", message);
}
}Select
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
let sleep_future = sleep(Duration::from_secs(1));
let mut count = 0;
tokio::select! {
_ = sleep_future => {
println!("Sleep finished");
}
_ = async {
while count < 10 {
count += 1;
}
} => {
println!("Count finished: {}", count);
}
}
}tokio::select! waits on multiple futures, proceeds when first completes.
Section 7: Iterators and Closures
Closures
Anonymous functions that capture their environment.
fn main() {
let expensive_closure = |num| {
println!("calculating slowly...");
std::thread::sleep(std::time::Duration::from_secs(2));
num
};
expensive_closure(5);
}Capturing environment:
fn main() {
let x = vec![1, 2, 3];
let equal_to_x = move |z| z == x; // Takes ownership of x
// println!("can't use x here: {:?}", x); // ❌ Error: x moved
let y = vec![1, 2, 3];
assert!(equal_to_x(y));
}Fn traits:
FnOnce: Consumes captured variablesFnMut: Mutably borrowsFn: Immutably borrows
Iterator Trait
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}Using iterators:
fn main() {
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
for val in v1_iter {
println!("Got: {}", val);
}
}Iterator Adapters
fn main() {
let v1: Vec<i32> = vec![1, 2, 3];
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
assert_eq!(v2, vec![2, 3, 4]);
}Common adapters:
map: Transform each elementfilter: Keep elements matching predicatetake: Take first n elementsskip: Skip first n elementsenumerate: Add indexzip: Combine two iterators
Consuming Adapters
fn main() {
let v1 = vec![1, 2, 3];
let total: i32 = v1.iter().sum();
assert_eq!(total, 6);
}Common consumers:
sum: Sum all elementscollect: Collect into collectioncount: Count elementsfold: Reduce to single value
Custom Iterators
struct Counter {
count: u32,
}
impl Counter {
fn new() -> Counter {
Counter { count: 0 }
}
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
if self.count < 5 {
self.count += 1;
Some(self.count)
} else {
None
}
}
}
fn main() {
let mut counter = Counter::new();
assert_eq!(counter.next(), Some(1));
assert_eq!(counter.next(), Some(2));
assert_eq!(counter.next(), Some(3));
assert_eq!(counter.next(), Some(4));
assert_eq!(counter.next(), Some(5));
assert_eq!(counter.next(), None);
}Section 8: Error Handling Patterns
Custom Error Types with thiserror
Add to Cargo.toml:
[dependencies]
thiserror = "1.0"Define custom error:
use thiserror::Error;
#[derive(Error, Debug)]
pub enum DataStoreError {
#[error("data store disconnected")]
Disconnect(#[from] std::io::Error),
#[error("the data for key `{0}` is not available")]
Redaction(String),
#[error("invalid header (expected {expected:?}, found {found:?})")]
InvalidHeader {
expected: String,
found: String,
},
#[error("unknown data store error")]
Unknown,
}Application Error Handling with anyhow
Add to Cargo.toml:
[dependencies]
anyhow = "1.0"Usage:
use anyhow::{Context, Result};
fn get_cluster_info() -> Result<String> {
let config = std::fs::read_to_string("cluster.json")
.context("Failed to read cluster config")?;
Ok(config)
}
fn main() -> Result<()> {
let info = get_cluster_info()?;
println!("Cluster info: {}", info);
Ok(())
}anyhow vs thiserror:
- thiserror: For libraries (define specific error types)
- anyhow: For applications (flexible error handling)
Section 9: Testing Strategies
Integration Tests
tests/integration_test.rs:
use my_crate;
#[test]
fn it_adds_two() {
assert_eq!(4, my_crate::add_two(2));
}Run integration tests:
cargo test --test integration_testDocumentation Tests
/// Adds one to the number given.
///
/// # Examples
///
/// ```
/// let arg = 5;
/// let answer = my_crate::add_one(arg);
///
/// assert_eq!(6, answer);
/// ```
pub fn add_one(x: i32) -> i32 {
x + 1
}Organizing Tests
my_crate/
├── src/
│ ├── lib.rs
│ └── main.rs
└── tests/
├── common/
│ └── mod.rs
└── integration_test.rstests/common/mod.rs:
pub fn setup() {
// Setup code shared between tests
}tests/integration_test.rs:
mod common;
#[test]
fn it_works() {
common::setup();
assert_eq!(2 + 2, 4);
}Summary
You’ve completed Intermediate Rust, covering 60-85% of Rust knowledge!
What You’ve Learned
- ✅ Generics: Type parameters, trait bounds, monomorphization
- ✅ Traits: Defining behavior, default implementations, trait bounds
- ✅ Lifetimes: Annotations, elision rules, struct lifetimes, ‘static
- ✅ Smart Pointers: Box, Rc, Arc, RefCell, interior mutability
- ✅ Concurrency: Threads, channels, Mutex, Arc, Send/Sync
- ✅ Async/Await: Tokio runtime, async functions, futures, select
- ✅ Iterators & Closures: Iterator trait, adapters, consumers, Fn traits
- ✅ Error Handling: thiserror, anyhow, context, propagation strategies
- ✅ Testing: Integration tests, documentation tests, test organization
Next Steps
Advanced Tutorial
Advanced Rust covers:
- Unsafe Rust and FFI
- Macros (declarative and procedural)
- Advanced trait patterns
- Memory layout and optimization
- Type-level programming
- WebAssembly compilation
Practical Resources
- Rust Cookbook - 30+ production-ready recipes
- How-To Guides - Domain-specific patterns
- Rust Best Practices - Industry standards
- Rust Anti-Patterns - Common mistakes to avoid
- Rust Cheat Sheet - Advanced syntax reference
- Rust Glossary - Technical terminology
- Complete Beginner’s Guide - Review fundamentals
Build Projects
- Concurrent web scraper with channels
- Async REST API with Actix or Axum
- CLI tool with structured error handling
- Multi-threaded data processor
You’re ready for production Rust! Continue to Advanced Tutorial for expert-level mastery.