| name | rust-patterns |
| description | Rust patterns covering ownership, lifetimes, error handling, traits, async with Tokio, and smart pointers. Activate when writing or reviewing Rust. |
| origin | FlowDeck |
Rust Patterns Skill
Safe, idiomatic Rust for production systems. Covers the ownership model, trait system, and async patterns.
When to Activate
Activate when:
- Writing new Rust crates or services
- Reviewing Rust code for safety and idiom
- Fighting the borrow checker and looking for solutions
- Designing async services with Tokio
- Choosing between smart pointer types
Ownership and Borrowing — Mental Model
Every value has exactly one owner. When the owner goes out of scope, the value is dropped. References borrow a value without taking ownership.
The Three Rules
- Each value has exactly one owner.
- There can be any number of shared (
&T) references, OR exactly one exclusive (&mut T) reference — never both at the same time.
- References must not outlive the value they point to.
fn main() {
let s1 = String::from("hello");
let s2 = s1;
let s3 = String::from("world");
let r1 = &s3;
let r2 = &s3;
println!("{r1} {r2}");
let mut s4 = String::from("mutable");
let r3 = &mut s4;
r3.push_str("!");
}
Clone When You Need a Copy
let original = vec![1, 2, 3];
let copy = original.clone();
#[derive(Clone, Copy)]
struct Point { x: f64, y: f64 }
let p1 = Point { x: 1.0, y: 2.0 };
let p2 = p1;
Lifetime Annotations
The compiler infers lifetimes in most cases via elision rules. Annotate when the compiler cannot determine the relationship.
When Annotations Are Required
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
struct Excerpt<'a> {
text: &'a str,
}
impl<'a> Excerpt<'a> {
fn content(&self) -> &str {
self.text
}
}
Elision Rules (No Annotation Needed)
fn first_word(s: &str) -> &str { ... }
fn trim(s: &str) -> &str { s.trim() }
'static — Only When Truly Static
let s: &'static str = "I live for the entire program";
Error Handling
Result<T, E> and the ? Operator
use std::fs;
use std::io;
fn read_config(path: &str) -> Result<String, io::Error> {
let content = fs::read_to_string(path)?;
Ok(content.trim().to_owned())
}
fn parse_port(s: &str) -> Result<u16, String> {
s.parse::<u16>()
.map_err(|e| format!("invalid port {s:?}: {e}"))
}
thiserror — Library Error Types
use thiserror::Error;
#[derive(Debug, Error)]
pub enum StoreError {
#[error("record {id} not found")]
NotFound { id: u64 },
#[error("database error")]
Database(#[from] sqlx::Error),
#[error("serialization failed: {0}")]
Serialize(#[from] serde_json::Error),
}
match store.get(id).await {
Err(StoreError::NotFound { id }) => respond_404(id),
Err(e) => respond_500(e),
Ok(record) => respond_200(record),
}
anyhow — Application Error Handling
use anyhow::{Context, Result};
async fn run() -> Result<()> {
let config = load_config("app.toml")
.context("failed to load application config")?;
let db = connect(&config.database_url).await
.context("database connection failed")?;
serve(db, config.port).await
}
let data = fetch(url).await
.with_context(|| format!("fetch failed for url: {url}"))?;
Trait System
impl Trait — Static Dispatch
fn make_greeting(name: &str) -> impl Display {
format!("Hello, {name}!")
}
fn print_all(items: &[impl Display]) {
for item in items {
println!("{item}");
}
}
dyn Trait — Dynamic Dispatch
fn make_handler(kind: &str) -> Box<dyn Handler> {
match kind {
"log" => Box::new(LogHandler::new()),
"audit" => Box::new(AuditHandler::new()),
_ => Box::new(NoopHandler),
}
}
Where Clauses for Readability
fn process<T: Debug + Clone + Send + 'static>(item: T) { ... }
fn process<T>(item: T)
where
T: Debug + Clone + Send + 'static,
{
...
}
Blanket Implementations
impl<T: Display> MyPrint for T {
fn print(&self) { println!("{self}"); }
}
Iterators
Iterators are lazy — no work until consumed. Chain adapters freely; the compiler fuses them.
let result: Vec<String> = (1..=10)
.filter(|n| n % 2 == 0)
.map(|n| n * n)
.take(3)
.map(|n| format!("n={n}"))
.collect();
let product: u64 = (1..=10).fold(1, |acc, n| acc * n);
let combined: Vec<i32> = vec![1, 2].into_iter()
.chain(vec![3, 4].into_iter())
.collect();
struct Counter { count: u32, max: u32 }
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<u32> {
if self.count < self.max {
self.count += 1;
Some(self.count)
} else {
None
}
}
}
Async/Await with Tokio
Spawning Tasks
use tokio::task;
#[tokio::main]
async fn main() {
let handle: task::JoinHandle<u64> = tokio::spawn(async {
compute_heavy_thing().await
});
let result = handle.await.expect("task panicked");
}
select! — Racing Futures
use tokio::select;
use tokio::time::{sleep, Duration};
async fn with_timeout<T>(fut: impl Future<Output = T>, ms: u64) -> Option<T> {
select! {
result = fut => Some(result),
_ = sleep(Duration::from_millis(ms)) => None,
}
}
Channels
use tokio::sync::{mpsc, oneshot};
let (tx, mut rx) = mpsc::channel::<String>(32);
tokio::spawn(async move {
while let Some(msg) = rx.recv().await {
process(msg).await;
}
});
tx.send("hello".to_owned()).await.unwrap();
let (resp_tx, resp_rx) = oneshot::channel::<Result<User, StoreError>>();
tokio::spawn(async move {
let user = db.find_user(id).await;
let _ = resp_tx.send(user);
});
let user = resp_rx.await.expect("worker dropped");
Smart Pointers
| Type | Ownership | Thread-safe | Interior mutability |
|---|
Box<T> | owned, heap | yes (if T: Send) | no |
Rc<T> | shared, heap | ❌ no | no |
Arc<T> | shared, heap | ✅ yes | no |
RefCell<T> | owned | ❌ no | ✅ runtime borrow |
Mutex<T> | owned | ✅ yes | ✅ locked |
RwLock<T> | owned | ✅ yes | ✅ locked |
let large: Box<[u8; 1_000_000]> = Box::new([0; 1_000_000]);
let shared = Arc::new(Config::load());
let clone = Arc::clone(&shared);
tokio::spawn(async move { use_config(clone).await });
let counter = Arc::new(Mutex::new(0u64));
let c = Arc::clone(&counter);
tokio::spawn(async move {
*c.lock().await += 1;
});
let node = Rc::new(RefCell::new(Node::new()));
node.borrow_mut().value = 42;
Enums and Pattern Matching
#[derive(Debug)]
enum Command {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(u8, u8, u8),
}
fn execute(cmd: Command) {
match cmd {
Command::Quit => println!("quit"),
Command::Move { x, y } => println!("move to ({x},{y})"),
Command::Write(msg) => println!("write: {msg}"),
Command::ChangeColor(r, g, b) => println!("color: #{r:02x}{g:02x}{b:02x}"),
}
}
if let Some(value) = map.get("key") {
println!("found: {value}");
}
let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() {
println!("{top}");
}
Cargo Workspace and Features
Workspace
[workspace]
members = ["crates/core", "crates/api", "crates/cli"]
resolver = "2"
[workspace.dependencies]
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
Conditional Compilation with Features
[features]
default = []
metrics = ["dep:prometheus"]
tracing = ["dep:tracing-subscriber"]
[dependencies]
prometheus = { version = "0.13", optional = true }
#[cfg(feature = "metrics")]
fn record_metric(name: &str, value: f64) {
prometheus::counter!(name).inc_by(value);
}
#[cfg(not(feature = "metrics"))]
fn record_metric(_name: &str, _value: f64) {}
Common Pitfalls
Fighting the Borrow Checker — Solutions
struct Grid { rows: Vec<Vec<i32>>, count: usize }
impl Grid {
fn swap_and_count(&mut self, r1: usize, r2: usize) {
let (rows, count) = (&mut self.rows, &mut self.count);
rows.swap(r1, r2);
*count += 1;
}
}
for item in &items {
process(item);
}
for item in items.iter() {
process(item);
}
String vs &str vs &[u8]
fn greet(name: &str) -> String { format!("Hello, {name}!") }
struct User { name: String }
fn checksum(data: &[u8]) -> u32 { ... }
Copy vs Clone
let x: i32 = 5;
let y = x;
#[derive(Clone)]
struct Config { }
let c1 = Config::load();
let c2 = c1.clone();