Rust interviews test your understanding of ownership, the borrow checker, lifetimes, traits, error handling, and safe concurrency. This guide covers the 50 most common questions — with clear answers and runnable code examples.
Quick reference
| Topic | Most asked questions |
|---|---|
| Ownership | Move semantics, Copy vs Clone, borrowing |
| Lifetimes | Annotations, lifetime elision, 'static |
| Traits | Trait objects, generics, impl vs dyn |
| Error handling | Result, ?, panic vs recoverable |
| Concurrency | Send/Sync, Arc/Mutex, channels |
| Async | async/await, Futures, Tokio |
| Memory | Stack vs heap, Box/Rc/Arc, no GC |
| Types | Structs, enums, pattern matching |
Ownership & Borrowing
1. What is Rust's ownership system and why does it exist?
Rust's ownership system is a set of compile-time rules that guarantee memory safety without a garbage collector:
| Rule | Meaning |
|---|---|
| Each value has one owner | No shared mutable state by default |
| Owner goes out of scope → value dropped | Automatic, deterministic memory cleanup |
| Transfer ownership with assignment | Move semantics prevent use-after-move |
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 is MOVED to s2
// println!("{}", s1); // ❌ compile error: use of moved value
println!("{}", s2); // ✅
}
Ownership eliminates dangling pointers, double-frees, and data races at compile time — zero runtime cost.
2. What is the difference between Copy and Clone?
| Trait | When used | Cost |
|---|---|---|
Copy |
Implicit bitwise copy on assignment | Zero (stack only) |
Clone |
Explicit .clone() call |
May allocate heap memory |
Types with Copy: integers, floats, booleans, char, tuples of Copy types.
Types without Copy: String, Vec, Box — they own heap data.
let x: i32 = 5;
let y = x; // copied, x still valid
println!("{} {}", x, y);
let s = String::from("hi");
let t = s.clone(); // explicit deep copy
println!("{} {}", s, t);
3. What are Rust's borrowing rules?
The borrow checker enforces two rules at all times:
- One mutable reference OR any number of immutable references — never both simultaneously.
- References must always be valid (no dangling references).
let mut v = vec![1, 2, 3];
// Multiple immutable borrows — OK
let a = &v;
let b = &v;
println!("{:?} {:?}", a, b);
// Mutable borrow — OK after immutable borrows end
let c = &mut v;
c.push(4);
// ❌ Compile error: can't have mut + immut at same time
let r1 = &v;
let r2 = &mut v; // error
println!("{}", r1);
4. What is the difference between &T, &mut T, and owned T?
| Type | Name | Allows mutation | Transfers ownership |
|---|---|---|---|
T |
Owned | Yes | Yes (move) |
&T |
Shared reference | No | No |
&mut T |
Mutable reference | Yes | No |
fn print_len(s: &String) { // borrows, doesn't take ownership
println!("{}", s.len());
}
fn make_uppercase(s: &mut String) { // mutably borrows
s.make_ascii_uppercase();
}
fn consume(s: String) { // takes ownership, s dropped on return
println!("{}", s);
}
5. What is a dangling reference and how does Rust prevent it?
A dangling reference points to memory that has been freed. Rust's borrow checker ensures references cannot outlive the data they point to:
// ❌ This will NOT compile
fn dangle() -> &String {
let s = String::from("hello"); // s is created
&s // return reference to s...
} // s is dropped here! reference would dangle
// ✅ Return owned value instead
fn no_dangle() -> String {
String::from("hello") // ownership moved to caller
}
Lifetimes
6. What are lifetimes in Rust?
Lifetimes are compile-time annotations that tell the borrow checker how long references are valid. They prevent dangling references without runtime overhead.
// 'a is a lifetime parameter — both input refs and output ref live at least 'a
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
fn main() {
let s1 = String::from("long string");
let result;
{
let s2 = String::from("xyz");
result = longest(s1.as_str(), s2.as_str());
println!("{}", result); // ✅ both alive here
}
// println!("{}", result); // ❌ s2 dropped, result would dangle
}
7. What is lifetime elision?
Lifetime elision is a set of rules that allow you to omit lifetime annotations in common patterns. The compiler infers them:
// These are equivalent:
fn first_word(s: &str) -> &str { ... }
fn first_word<'a>(s: &'a str) -> &'a str { ... }
Elision rules:
- Each input reference gets its own lifetime.
- If there is exactly one input lifetime, it applies to all outputs.
- If one input is
&selfor&mut self, its lifetime applies to outputs.
8. What is 'static lifetime?
'static means the reference is valid for the entire program duration.
// String literals are 'static — stored in binary's read-only data
let s: &'static str = "hello world";
// Trait objects sometimes require 'static
fn get_formatter() -> Box<dyn std::fmt::Display + 'static> {
Box::new(42)
}
Common use: T: 'static means T contains no non-static references (often required for threads).
9. How do you add lifetime annotations to structs?
When a struct holds a reference, it needs a lifetime parameter:
struct Excerpt<'a> {
part: &'a str, // struct can't outlive the string it borrows from
}
impl<'a> Excerpt<'a> {
fn level(&self) -> i32 { 3 }
fn announce(&self, announcement: &str) -> &str {
println!("Attention: {}", announcement);
self.part // 'a elided — same as &'a str
}
}
Traits
10. What are traits in Rust?
Traits define shared behaviour — similar to interfaces in other languages but more powerful. They can have default implementations and be used as bounds.
trait Greet {
fn name(&self) -> &str;
// Default implementation
fn hello(&self) {
println!("Hello, {}!", self.name());
}
}
struct Person { name: String }
impl Greet for Person {
fn name(&self) -> &str { &self.name }
// hello() inherited for free
}
let p = Person { name: "Alice".into() };
p.hello(); // "Hello, Alice!"
11. What is the difference between impl Trait and dyn Trait?
| Feature | impl Trait |
dyn Trait |
|---|---|---|
| Dispatch | Static (monomorphisation) | Dynamic (vtable) |
| Performance | Zero-cost, inlined | Small overhead |
| Flexibility | Single concrete type | Any type at runtime |
| Return pos | Can only return one concrete type | Multiple types possible |
| Allocation | Stack (usually) | Heap via Box<dyn Trait> |
// impl Trait — compiler generates specialised code per type
fn notify(item: &impl Summary) { println!("{}", item.summarize()); }
// dyn Trait — runtime dispatch via vtable
fn notify_dyn(item: &dyn Summary) { println!("{}", item.summarize()); }
// Returning impl Trait (single concrete type)
fn make_summarizer() -> impl Summary { Article { ... } }
// Returning dyn Trait (multiple possible types)
fn make_summarizer_dyn(flag: bool) -> Box<dyn Summary> {
if flag { Box::new(Article { ... }) } else { Box::new(Tweet { ... }) }
}
12. What are trait bounds?
Trait bounds constrain generic types to only those that implement a given trait:
// Syntax 1: inline bound
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list { if item > largest { largest = item; } }
largest
}
// Syntax 2: where clause (cleaner with multiple bounds)
fn print_info<T>(item: T) where T: Display + Debug + Clone {
println!("{:?}", item);
}
// Multiple bounds with +
fn notify(item: &(impl Summary + Display)) { ... }
13. What are common standard library traits?
| Trait | Purpose |
|---|---|
Display |
{} formatting |
Debug |
{:?} formatting |
Clone |
Explicit deep copy |
Copy |
Implicit bitwise copy |
PartialEq / Eq |
== operator |
PartialOrd / Ord |
<, > operators |
Iterator |
.next() — enables for loops |
From / Into |
Type conversions |
Default |
Zero/empty value |
Drop |
Custom destructor |
Send |
Safe to transfer across threads |
Sync |
Safe to share reference across threads |
14. What is the Iterator trait?
Any type implementing Iterator with a next() method gets all iterator adapters for free:
struct Counter { count: u32 }
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<u32> {
self.count += 1;
if self.count <= 5 { Some(self.count) } else { None }
}
}
let sum: u32 = Counter::new()
.zip(Counter::new().skip(1))
.map(|(a, b)| a * b)
.filter(|x| x % 3 == 0)
.sum();
Enums & Pattern Matching
15. How do Rust enums differ from enums in other languages?
Rust enums are algebraic data types — each variant can hold different data:
enum Shape {
Circle(f64), // tuple variant
Rectangle { width: f64, height: f64 }, // struct variant
Triangle(f64, f64, f64), // tuple variant with 3 fields
Point, // unit variant (no data)
}
fn area(shape: &Shape) -> f64 {
match shape {
Shape::Circle(r) => std::f64::consts::PI * r * r,
Shape::Rectangle { width, height } => width * height,
Shape::Triangle(a, b, c) => {
let s = (a + b + c) / 2.0;
(s * (s - a) * (s - b) * (s - c)).sqrt()
}
Shape::Point => 0.0,
}
}
16. What is Option<T> and why does Rust use it instead of null?
Option<T> forces you to handle the absence of a value — null pointer exceptions are impossible:
enum Option<T> { Some(T), None }
fn find_user(id: u32) -> Option<String> {
if id == 1 { Some("Alice".into()) } else { None }
}
// Must handle both cases
match find_user(42) {
Some(name) => println!("Found: {}", name),
None => println!("Not found"),
}
// Shorthand methods
let name = find_user(1).unwrap_or("unknown".into());
let upper = find_user(1).map(|n| n.to_uppercase());
let len = find_user(1).as_ref().map(|n| n.len());
// ? operator — propagate None
fn greeting(id: u32) -> Option<String> {
let name = find_user(id)?; // returns None if not found
Some(format!("Hello, {}!", name))
}
17. What is Result<T, E> and how is error handling done in Rust?
Result<T, E> is the standard type for recoverable errors:
use std::fs;
use std::io;
fn read_username() -> Result<String, io::Error> {
let content = fs::read_to_string("user.txt")?; // ? propagates error
Ok(content.trim().to_string())
}
// Handling results
match read_username() {
Ok(name) => println!("User: {}", name),
Err(e) => eprintln!("Error: {}", e),
}
// Chainable methods
let upper = read_username()
.map(|s| s.to_uppercase())
.unwrap_or_else(|_| "DEFAULT".into());
18. When should you use panic! vs returning Result?
Use panic! |
Use Result |
|---|---|
| Unrecoverable bug (programming error) | Expected failure condition |
| Test failures | I/O errors, parse errors |
| Prototype / POC code | Library code |
| Invariant violated (index out of bounds) | User input validation |
// panic — for programmer errors
fn divide(a: f64, b: f64) -> f64 {
if b == 0.0 { panic!("Division by zero!"); }
a / b
}
// Result — for expected failures
fn parse_age(s: &str) -> Result<u8, String> {
s.parse::<u8>().map_err(|e| format!("Invalid age: {}", e))
}
19. What is the ? operator?
? is syntactic sugar for "propagate the error if Err/None, otherwise unwrap":
// Without ?
fn read_number(path: &str) -> Result<i32, Box<dyn std::error::Error>> {
let content = match std::fs::read_to_string(path) {
Ok(s) => s,
Err(e) => return Err(Box::new(e)),
};
match content.trim().parse::<i32>() {
Ok(n) => Ok(n),
Err(e) => Err(Box::new(e)),
}
}
// With ? — equivalent, much cleaner
fn read_number(path: &str) -> Result<i32, Box<dyn std::error::Error>> {
let content = std::fs::read_to_string(path)?;
Ok(content.trim().parse::<i32>()?)
}
Memory Management
20. How does Rust manage memory without a garbage collector?
Rust uses ownership + RAII (Resource Acquisition Is Initialization):
- Each value has a single owner.
- When the owner goes out of scope, the value's
Dropimpl runs and memory is freed. - No GC pauses, no runtime overhead.
{
let s = String::from("hello"); // heap allocated
// use s
} // s.drop() called automatically — heap memory freed
// Custom Drop
struct Connection { id: u32 }
impl Drop for Connection {
fn drop(&mut self) { println!("Closing connection {}", self.id); }
}
21. What are Box<T>, Rc<T>, and Arc<T>?
| Type | Ownership | Thread-safe | Use case |
|---|---|---|---|
Box<T> |
Single owner | Yes | Heap allocation, recursive types |
Rc<T> |
Multiple owners | ❌ No | Single-threaded shared ownership |
Arc<T> |
Multiple owners | ✅ Yes | Multi-threaded shared ownership |
// Box — heap allocation
let b = Box::new(5);
println!("{}", b); // auto-derefs
// Rc — reference-counted, single thread
use std::rc::Rc;
let a = Rc::new(5);
let b = Rc::clone(&a); // ref count: 2
println!("{} {}", a, b);
// Arc — atomic Rc, multi-thread safe
use std::sync::Arc;
let data = Arc::new(vec![1, 2, 3]);
let data2 = Arc::clone(&data);
std::thread::spawn(move || println!("{:?}", data2));
22. What is RefCell<T> and interior mutability?
RefCell<T> allows mutating data through a shared reference — enforcing borrow rules at runtime instead of compile time:
use std::cell::RefCell;
let data = RefCell::new(vec![1, 2, 3]);
// Borrow immutably
let r1 = data.borrow();
println!("{:?}", r1);
drop(r1); // must release before mutable borrow
// Borrow mutably
data.borrow_mut().push(4);
println!("{:?}", data.borrow());
// Rc<RefCell<T>> — multiple owners with interior mutability
use std::rc::Rc;
let shared = Rc::new(RefCell::new(0));
let clone = Rc::clone(&shared);
*clone.borrow_mut() += 1;
println!("{}", shared.borrow()); // 1
Concurrency
23. How does Rust achieve fearless concurrency?
Rust's type system makes data races impossible to compile:
Send: a type is safe to move to another thread.Sync: a type is safe to share references across threads.
The compiler automatically derives Send/Sync for most types and rejects unsafe patterns:
// ❌ Cannot send Rc across threads — it's not Send
let rc = std::rc::Rc::new(1);
std::thread::spawn(move || println!("{}", rc)); // compile error
// ✅ Arc is Send + Sync
let arc = std::sync::Arc::new(1);
let arc2 = arc.clone();
std::thread::spawn(move || println!("{}", arc2));
24. How do you share mutable state between threads?
Use Arc<Mutex<T>> — atomic reference counting + mutual exclusion:
use std::sync::{Arc, Mutex};
use std::thread;
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let c = Arc::clone(&counter);
handles.push(thread::spawn(move || {
let mut num = c.lock().unwrap(); // blocks until lock acquired
*num += 1;
}));
}
for h in handles { h.join().unwrap(); }
println!("Result: {}", *counter.lock().unwrap()); // 10
Use RwLock<T> when reads are frequent and writes are rare.
25. What are channels in Rust?
Channels implement message passing — a safe alternative to shared memory:
use std::sync::mpsc; // multiple producer, single consumer
use std::thread;
let (tx, rx) = mpsc::channel();
// Clone sender for multiple producers
let tx2 = tx.clone();
thread::spawn(move || tx.send("hello from thread 1").unwrap());
thread::spawn(move || tx2.send("hello from thread 2").unwrap());
// Receive (blocks until message arrives)
for _ in 0..2 {
println!("{}", rx.recv().unwrap());
}
Async / Await
26. How does async/await work in Rust?
Rust's async is based on zero-cost Futures — they are state machines compiled at build time, not runtime threads:
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
let result = fetch_data().await;
println!("{}", result);
}
async fn fetch_data() -> String {
sleep(Duration::from_millis(100)).await;
"data ready".to_string()
}
Key points:
async fnreturnsimpl Future<Output = T>.awaitsuspends the current task, not the thread- Requires an async runtime (Tokio, async-std)
27. What is a Future in Rust?
A Future is a value that may not be ready yet — similar to Promise in JS:
trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
Futures are lazy — nothing happens until you .await them or pass them to a runtime executor.
// Concurrent futures with tokio::join!
async fn fetch_all() {
let (a, b, c) = tokio::join!(
fetch_user(1),
fetch_posts(1),
fetch_comments(1),
);
}
// Parallel tasks
async fn fetch_parallel() {
let handle1 = tokio::spawn(fetch_user(1));
let handle2 = tokio::spawn(fetch_posts(1));
let (u, p) = (handle1.await.unwrap(), handle2.await.unwrap());
}
28. What is Pin<T> and why is it needed?
Pin<T> prevents a value from being moved in memory — required for self-referential structs that async state machines can become:
use std::pin::Pin;
use std::future::Future;
// Most async code doesn't need Pin directly
// The compiler handles it when you use async/await
// Manually pinning (advanced)
let fut = async { 42 };
let mut boxed: Pin<Box<dyn Future<Output = i32>>> = Box::pin(fut);
In practice, you mostly encounter Pin when implementing Future manually or working with tokio::pin!.
Error Handling (Advanced)
29. How do you create custom error types?
use std::fmt;
#[derive(Debug)]
enum AppError {
NotFound(String),
ParseError(std::num::ParseIntError),
IoError(std::io::Error),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
AppError::NotFound(s) => write!(f, "Not found: {}", s),
AppError::ParseError(e) => write!(f, "Parse error: {}", e),
AppError::IoError(e) => write!(f, "IO error: {}", e),
}
}
}
// Implement From for automatic ? conversion
impl From<std::num::ParseIntError> for AppError {
fn from(e: std::num::ParseIntError) -> Self { AppError::ParseError(e) }
}
fn parse_id(s: &str) -> Result<u32, AppError> {
Ok(s.parse::<u32>()?) // auto-converts ParseIntError → AppError
}
30. What is the thiserror and anyhow crates used for?
| Crate | Use case | When to use |
|---|---|---|
thiserror |
Library error types | When callers need to match on errors |
anyhow |
Application error handling | When you just need to propagate errors |
// thiserror — clean derive macro for error types
use thiserror::Error;
#[derive(Error, Debug)]
enum MyError {
#[error("File not found: {0}")]
NotFound(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
}
// anyhow — ergonomic for application code
use anyhow::{Context, Result};
fn read_config() -> Result<Config> {
let text = std::fs::read_to_string("config.toml")
.context("Failed to read config file")?;
Ok(toml::from_str(&text)?)
}
Structs & Generics
31. What are Rust structs and how do they differ from classes?
Rust structs hold data; behaviour is added through impl blocks and traits — no inheritance:
#[derive(Debug, Clone, PartialEq)]
struct Point {
x: f64,
y: f64,
}
impl Point {
// Associated function (constructor)
fn new(x: f64, y: f64) -> Self { Point { x, y } }
// Method — takes &self
fn distance(&self, other: &Point) -> f64 {
((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt()
}
// Mutable method
fn translate(&mut self, dx: f64, dy: f64) {
self.x += dx;
self.y += dy;
}
}
let mut p = Point::new(0.0, 0.0);
p.translate(3.0, 4.0);
println!("{:.1}", p.distance(&Point::new(0.0, 0.0))); // 5.0
32. How do generics work in Rust?
Generics are monomorphised at compile time — zero runtime cost:
// Generic function
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list { if item > largest { largest = item; } }
largest
}
// Generic struct
struct Pair<T> {
first: T,
second: T,
}
impl<T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
if self.first >= self.second {
println!("largest: {}", self.first);
} else {
println!("largest: {}", self.second);
}
}
}
33. What are associated types in traits?
Associated types are a way to bind a type to a trait without making the trait generic:
trait Container {
type Item; // associated type
fn get(&self, i: usize) -> Option<&Self::Item>;
fn len(&self) -> usize;
}
struct Stack<T>(Vec<T>);
impl<T> Container for Stack<T> {
type Item = T;
fn get(&self, i: usize) -> Option<&T> { self.0.get(i) }
fn len(&self) -> usize { self.0.len() }
}
Use associated types when a trait has exactly one logical output type; use generic parameters when multiple implementations for the same type make sense.
Closures & Iterators
34. What are closures in Rust and how do they capture variables?
Closures can capture environment by reference, mutable reference, or by moving ownership:
let x = 5;
// Captures &x (Fn trait)
let print = || println!("{}", x);
print();
// Captures &mut counter (FnMut trait)
let mut counter = 0;
let mut inc = || { counter += 1; counter };
println!("{}", inc()); // 1
// Captures ownership with `move` (FnOnce trait)
let s = String::from("hello");
let consume = move || println!("{}", s);
// s is no longer accessible here
consume();
35. What is the difference between Fn, FnMut, and FnOnce?
| Trait | Captures | Can call |
|---|---|---|
Fn |
By reference | Multiple times |
FnMut |
By mutable reference | Multiple times (mutating) |
FnOnce |
By move (ownership) | Only once |
fn apply<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 { f(x) }
fn apply_mut<F: FnMut(i32) -> i32>(mut f: F, x: i32) -> i32 { f(x) }
fn apply_once<F: FnOnce(i32) -> i32>(f: F, x: i32) -> i32 { f(x) }
36. How do Rust iterators work?
Rust iterators are lazy — adaptors build a pipeline, nothing executes until a consuming method is called:
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let result: Vec<i32> = numbers.iter()
.filter(|&&x| x % 2 == 0) // lazy
.map(|&x| x * x) // lazy
.take(3) // lazy
.collect(); // consuming — executes everything
println!("{:?}", result); // [4, 16, 36]
// Other consuming methods
let sum: i32 = numbers.iter().sum();
let any_big = numbers.iter().any(|&x| x > 5);
let found = numbers.iter().find(|&&x| x > 5); // Option<&i32>
Advanced Topics
37. What are Rust's smart pointers?
| Smart Pointer | Description |
|---|---|
Box<T> |
Heap allocation, single owner |
Rc<T> |
Multiple owners (single-threaded) |
Arc<T> |
Multiple owners (thread-safe) |
Cell<T> |
Interior mutability for Copy types |
RefCell<T> |
Interior mutability with runtime borrow check |
Mutex<T> |
Mutual exclusion for threads |
RwLock<T> |
Multiple readers or one writer |
38. What is unsafe Rust?
unsafe blocks allow operations that the compiler can't verify are safe:
unsafe fn dangerous() { ... }
let mut v = vec![1, 2, 3];
let r = &mut v[0] as *mut i32; // raw pointer
unsafe {
*r = 42; // dereferencing raw pointer
dangerous();
}
Five things only unsafe allows:
- Dereference raw pointers
- Call unsafe functions
- Access/modify mutable static variables
- Implement unsafe traits
- Access fields of unions
The invariants broken by unsafe must be upheld by the programmer — the compiler trusts you.
39. What are macros in Rust?
Rust has two types of macros — they operate on the AST before compilation:
// Declarative macros (macro_rules!)
macro_rules! hello {
() => { println!("Hello!"); };
($name:expr) => { println!("Hello, {}!", $name); };
}
hello!(); // "Hello!"
hello!("Alice"); // "Hello, Alice!"
// Procedural macros (derive macros)
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Config { host: String, port: u16 }
Common built-in macros: vec![], println!, format!, assert!, todo!, unimplemented!, dbg!.
40. What is the newtype pattern?
Wrapping a type in a struct to create a distinct type — adds type safety and allows implementing traits:
struct Meters(f64);
struct Kilograms(f64);
// Can't accidentally add meters and kilograms
fn calculate(distance: Meters, _weight: Kilograms) -> f64 {
distance.0 / 9.8
}
// Implement Display on external type (orphan rule workaround)
use std::fmt;
struct Wrapper(Vec<String>);
impl fmt::Display for Wrapper {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}]", self.0.join(", "))
}
}
41. What is the orphan rule?
You can only implement a trait for a type if either the trait or the type is defined in your crate:
// ✅ Your trait, external type
impl MyTrait for Vec<i32> { ... }
// ✅ External trait, your type
impl Display for MyStruct { ... }
// ❌ Orphan: neither is yours
impl Display for Vec<i32> { ... } // compile error
The newtype pattern wraps external types to work around this.
42. What is Deref coercion?
Rust automatically dereferences smart pointers when needed:
fn hello(name: &str) { println!("Hello, {}!", name); }
let s = String::from("Alice");
hello(&s); // &String → &str (Deref coercion)
let b = Box::new(s);
hello(&b); // &Box<String> → &String → &str (chained)
Deref coercions happen automatically in function calls, method calls, and some other contexts.
Testing
43. How do you write tests in Rust?
// Unit test in same file
fn add(a: i32, b: i32) -> i32 { a + b }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
#[test]
#[should_panic(expected = "overflow")]
fn test_overflow() {
let _: u8 = 200u8 + 100u8; // panics in debug mode
}
#[test]
fn test_result() -> Result<(), String> {
let x = "5".parse::<i32>().map_err(|e| e.to_string())?;
assert_eq!(x, 5);
Ok(())
}
}
cargo test # run all tests
cargo test test_add # run specific test
cargo test -- --nocapture # show println! output
44. What are integration tests in Rust?
Integration tests live in the tests/ directory and test the public API:
// tests/integration_test.rs
use my_library::add;
#[test]
fn it_adds_two() {
assert_eq!(add(2, 2), 4);
}
my_project/
├── src/lib.rs
├── tests/
│ └── integration_test.rs
└── Cargo.toml
Cargo & Ecosystem
45. What is Cargo and what are its key commands?
Cargo is Rust's build system and package manager:
| Command | Description |
|---|---|
cargo new project |
Create new binary project |
cargo new --lib lib |
Create new library crate |
cargo build |
Compile (debug mode) |
cargo build --release |
Optimised build |
cargo run |
Build and run |
cargo test |
Run tests |
cargo bench |
Run benchmarks |
cargo check |
Type-check without building |
cargo clippy |
Lint with Clippy |
cargo fmt |
Format with rustfmt |
cargo doc --open |
Generate and open docs |
cargo add serde |
Add dependency |
cargo update |
Update dependencies |
cargo publish |
Publish to crates.io |
46. What is the difference between a binary and library crate?
| Feature | Binary (src/main.rs) |
Library (src/lib.rs) |
|---|---|---|
| Entry point | fn main() |
No main |
| Executable | Yes | No (.rlib / .so) |
| Published | Optional | Primary target |
| Can have both | Yes — same Cargo.toml |
# Cargo.toml
[package]
name = "my-project"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
Common Rust Patterns
47. What is the Builder pattern in Rust?
#[derive(Debug)]
struct Config {
host: String,
port: u16,
timeout_ms: u64,
}
struct ConfigBuilder {
host: String,
port: u16,
timeout_ms: u64,
}
impl ConfigBuilder {
fn new() -> Self {
ConfigBuilder { host: "localhost".into(), port: 8080, timeout_ms: 5000 }
}
fn host(mut self, h: &str) -> Self { self.host = h.into(); self }
fn port(mut self, p: u16) -> Self { self.port = p; self }
fn timeout_ms(mut self, t: u64) -> Self { self.timeout_ms = t; self }
fn build(self) -> Config {
Config { host: self.host, port: self.port, timeout_ms: self.timeout_ms }
}
}
let cfg = ConfigBuilder::new()
.host("example.com")
.port(443)
.timeout_ms(3000)
.build();
48. What is the type state pattern?
Using types to enforce valid state transitions at compile time:
struct Locked;
struct Unlocked;
struct Vault<State> { _state: std::marker::PhantomData<State> }
impl Vault<Locked> {
fn new() -> Self { Vault { _state: std::marker::PhantomData } }
fn unlock(self, _pin: &str) -> Vault<Unlocked> {
Vault { _state: std::marker::PhantomData }
}
}
impl Vault<Unlocked> {
fn withdraw(&self, amount: f64) { println!("Withdrew ${}", amount); }
fn lock(self) -> Vault<Locked> { Vault { _state: std::marker::PhantomData } }
}
let vault = Vault::new();
// vault.withdraw(100.0); // ❌ compile error — vault is Locked
let vault = vault.unlock("1234");
vault.withdraw(100.0); // ✅
let _vault = vault.lock();
Anti-patterns
49. What are common Rust anti-patterns?
| Anti-pattern | Problem | Better approach |
|---|---|---|
.unwrap() everywhere |
Panics in production | Use ?, map_err, or handle errors |
clone() to fix borrow errors |
Unnecessary heap allocation | Restructure ownership or use references |
Rc<RefCell<T>> by default |
Runtime panics, hard to reason about | Use ownership + &mut when possible |
String parameter instead of &str |
Forces allocation | Accept &str, convert inside if needed |
| Mutex deadlocks | Lock held across await | Drop lock before .await |
unsafe for convenience |
Unsound code | Look for safe alternatives first |
Ignoring clippy warnings |
Subtle bugs | cargo clippy -- -D warnings in CI |
Not using #[derive] |
Boilerplate | Derive Debug, Clone, PartialEq |
Rust vs Other Languages
50. How does Rust compare to C++, Go, and Python for systems programming?
| Feature | Rust | C++ | Go | Python |
|---|---|---|---|---|
| Memory safety | Compile-time guaranteed | Manual (UB possible) | GC | GC |
| Performance | C-like | Best-in-class | Near-C | 10–100× slower |
| Concurrency safety | Data races impossible | Manual | Goroutines (safe) | GIL limits |
| Null safety | Option<T> (no nulls) |
Null pointers | nil (runtime panic) | None (runtime) |
| Error handling | Result<T,E> |
Exceptions / error codes | Multiple returns | Exceptions |
| Compile time | Slower (complex checks) | Slow (templates) | Fast | N/A (interpreted) |
| Learning curve | Steep (ownership/borrow) | Steep (UB, templates) | Gentle | Very gentle |
| Async model | Futures (zero-cost) | coroutines (C++20) | Goroutines | asyncio |
| Ecosystem | Growing (crates.io) | Mature | Mature (stdlib) | Largest |
| Best for | Systems, WASM, embedded | Games, OS, HFT | Cloud, CLIs, APIs | ML, scripting |
Common mistakes
| Mistake | Why it's wrong | Fix |
|---|---|---|
Using String when &str suffices |
Forces heap allocation | Accept &str, return String only if owned |
Holding MutexGuard across .await |
Deadlock in async code | Drop guard before await point |
Using index[] on slices/vecs |
Panics on out-of-bounds | Use .get() returning Option |
| Cloning to satisfy borrow checker | Hides design issues | Restructure lifetimes or ownership |
Arc<Mutex<>> when not needed |
Unnecessary overhead | Use &mut or channels instead |
Implementing Drop on Copy types |
Compile error | Remove Copy impl or remove Drop |
Forgetting mut on function params |
Can't mutate inside function | Add mut to binding |
Using 'static lifetime everywhere |
Overly restrictive | Use the correct lifetime parameter |
Rust vs alternatives
| Language | Memory | Speed | Safety | Ergonomics | Best use |
|---|---|---|---|---|---|
| Rust | Manual (ownership) | ~C | Compile-time | Moderate | Systems, WASM, embedded |
| C | Manual (unsafe) | Fastest | None | Low | OS kernels, drivers |
| C++ | Manual/smart ptrs | ~Rust | Partial | Moderate | Games, HFT, HPC |
| Go | GC | ~2-5× slower | Runtime | High | Cloud, APIs, DevOps |
| Java/JVM | GC | ~3-10× slower | Runtime | High | Enterprise, Android |
| Python | GC | ~50-100× slower | Runtime | Very high | ML, scripting, data |
| Zig | Manual (no RAII) | ~Rust | Partial | Moderate | Systems (simpler than Rust) |
FAQ
Q: Is Rust's learning curve worth it?
The ownership/borrow checker is challenging but eliminates whole classes of bugs: buffer overflows, use-after-free, data races, null dereferences. Once it clicks, you write safer code in all languages. Teams report fewer production bugs and confident refactoring.
Q: When should I use async Rust vs threads?
Use async for I/O-bound work with high concurrency (thousands of connections). Use threads for CPU-bound parallelism. Mix both: tokio::task::spawn_blocking runs blocking code in a thread pool without blocking the async runtime.
Q: What is the difference between String and &str?String is an owned, heap-allocated, growable UTF-8 string. &str is a borrowed slice of UTF-8 bytes — it can point into a String, a string literal (stored in binary), or any UTF-8 buffer. Prefer &str for function parameters; return String when the function creates the data.
Q: What is monomorphisation and does it cause code bloat?
Monomorphisation means the compiler generates a separate copy of generic code for each concrete type used. This enables zero-cost abstractions but can increase binary size. Use dyn Trait to avoid monomorphisation where code size matters more than performance.
Q: How do I avoid fighting the borrow checker?
- Prefer smaller, shorter-lived borrows. 2. Return owned values instead of references when possible. 3. Use
.clone()initially, optimise later. 4. Restructure code so borrows don't overlap. 5. Usesplit_at_mutfor multiple mutable slices. 6. Read the error messages — they are excellent.
Q: What Rust crates should I know for backend development?tokio (async runtime), axum or actix-web (web framework), sqlx or diesel (database), serde / serde_json (serialisation), reqwest (HTTP client), tracing (observability), thiserror / anyhow (error handling), tower (middleware), clap (CLI parsing).