Perguntas de Entrevista sobre Rust: Guia Completo 2026

As 25 perguntas mais comuns em entrevistas sobre Rust. Ownership, borrowing, lifetimes, traits, async e concorrencia com respostas detalhadas e exemplos de codigo.

Perguntas de Entrevista sobre Rust - Guia Completo

As entrevistas sobre Rust avaliam a compreensao do sistema unico de ownership, o gerenciamento de memoria sem coletor de lixo e a capacidade de escrever codigo concorrente seguro. Este guia cobre as perguntas essenciais, desde os fundamentos do ownership ate padroes avancados de async e concorrencia.

Dica para a entrevista

Os entrevistadores valorizam explicacoes que demonstram compreensao das garantias de seguranca de memoria do Rust. Explicar como o compilador previne erros em tempo de compilacao faz toda a diferenca.

Ownership e Borrowing

Pergunta 1: Explicar o sistema de ownership do Rust

O ownership e o conceito central do Rust que permite o gerenciamento de memoria sem coletor de lixo, garantindo seguranca de memoria em tempo de compilacao.

ownership_basics.rsrust
// The three fundamental rules of ownership

fn main() {
    // Rule 1: Each value has a single owner
    let s1 = String::from("hello");  // s1 is the owner

    // Rule 2: Only one variable can own a value at a time
    let s2 = s1;  // s1 is MOVED to s2
    // println!("{}", s1);  // ERROR: s1 is no longer valid
    println!("{}", s2);  // OK: s2 is now the owner

    // Rule 3: When the owner goes out of scope, the value is dropped
    {
        let s3 = String::from("world");
        // s3 is valid here
    }  // s3 goes out of scope, memory is automatically freed

    // Copy types: simple types are copied, not moved
    let x = 5;
    let y = x;  // x is COPIED, not moved
    println!("x = {}, y = {}", x, y);  // Both are valid
}

// Move in action with functions
fn take_ownership(s: String) {
    // s takes ownership of the String
    println!("{}", s);
}  // s is dropped here, memory freed

fn makes_copy(i: i32) {
    // i is a copy of the argument
    println!("{}", i);
}  // i goes out of scope, nothing special (Copy type)

fn ownership_with_functions() {
    let s = String::from("hello");
    take_ownership(s);  // s is moved into the function
    // println!("{}", s);  // ERROR: s is no longer valid

    let x = 5;
    makes_copy(x);  // x is copied
    println!("{}", x);  // OK: x is still valid
}

O ownership elimina erros comuns de memoria: use-after-free, double-free e memory leaks. O compilador garante essas propriedades em tempo de compilacao.

Pergunta 2: Qual e a diferenca entre borrowing imutavel e mutavel?

O borrowing permite utilizar um valor sem assumir o ownership, com regras rigorosas para prevenir data races.

borrowing_rules.rsrust
// Immutable and mutable references

fn main() {
    let mut s = String::from("hello");

    // IMMUTABLE REFERENCES (&T)
    // Can coexist in unlimited numbers
    let r1 = &s;  // immutable reference
    let r2 = &s;  // another immutable reference
    println!("{} and {}", r1, r2);  // OK

    // MUTABLE REFERENCE (&mut T)
    // Only one at a time, and no simultaneous immutable references
    let r3 = &mut s;  // mutable reference
    // let r4 = &s;  // ERROR: cannot have both immutable and mutable
    // let r5 = &mut s;  // ERROR: only one mutable reference allowed
    r3.push_str(" world");
    println!("{}", r3);

    // Reference scopes are limited to their last use
    let r6 = &s;  // OK because r3 is no longer used
    println!("{}", r6);
}

// Practical example: modifying a struct
struct User {
    name: String,
    age: u32,
}

impl User {
    // &self: read-only access
    fn get_name(&self) -> &str {
        &self.name
    }

    // &mut self: modification access
    fn set_name(&mut self, name: String) {
        self.name = name;
    }

    // self: takes ownership (consumes the instance)
    fn into_name(self) -> String {
        self.name  // The User instance no longer exists after this
    }
}

fn borrowing_with_structs() {
    let mut user = User {
        name: String::from("Alice"),
        age: 30,
    };

    // Reading
    println!("Name: {}", user.get_name());

    // Modifying
    user.set_name(String::from("Bob"));

    // Consuming
    let name = user.into_name();
    // user.age;  // ERROR: user has been consumed
}

Essas regras garantem a ausencia de data races em tempo de compilacao. Nenhuma outra linguagem oferece essa garantia sem sacrificar desempenho.

Non-Lexical Lifetimes

Desde o Rust 2018, o compilador utiliza NLL (Non-Lexical Lifetimes) para determinar com maior precisao quando uma referencia nao esta mais em uso, permitindo maior flexibilidade.

Pergunta 3: O que sao lifetimes e quando devem ser anotados?

Os lifetimes sao anotacoes que indicam ao compilador por quanto tempo as referencias sao validas, prevenindo referencias pendentes.

lifetimes.rsrust
// Understanding and annotating lifetimes

// ERROR: dangling reference
// fn dangling() -> &String {
//     let s = String::from("hello");
//     &s  // s is dropped at function end, reference invalid
// }

// The compiler often infers lifetimes automatically
fn first_word(s: &str) -> &str {
    // Elided lifetime: compiler understands the return
    // has the same lifetime as the input
    match s.find(' ') {
        Some(i) => &s[..i],
        None => s,
    }
}

// Explicit annotation needed with multiple references
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    // 'a means: the return lives at least as long
    // as the shorter of the two inputs
    if x.len() > y.len() { x } else { y }
}

fn lifetime_example() {
    let string1 = String::from("long string");
    let result;

    {
        let string2 = String::from("xyz");
        result = longest(&string1, &string2);
        println!("Longest: {}", result);  // OK here
    }
    // println!("{}", result);  // ERROR: string2 is dropped
}

// Lifetimes in structs
struct ImportantExcerpt<'a> {
    part: &'a str,  // Struct cannot outlive part
}

impl<'a> ImportantExcerpt<'a> {
    // Method returning a reference with the same lifetime
    fn level(&self) -> i32 {
        3
    }

    // Elided lifetime for &self returning a new reference
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention: {}", announcement);
        self.part  // Returns with lifetime 'a
    }
}

// Static lifetime: lives for the entire program duration
fn static_lifetime() {
    let s: &'static str = "hello";  // Stored in the binary

    // Constants have implicit 'static lifetime
    const MAX_POINTS: u32 = 100_000;
}

// Combining lifetimes and generics
fn longest_with_announcement<'a, T>(
    x: &'a str,
    y: &'a str,
    ann: T,
) -> &'a str
where
    T: std::fmt::Display,
{
    println!("Announcement: {}", ann);
    if x.len() > y.len() { x } else { y }
}

Os lifetimes sao verificados em tempo de compilacao. Se o codigo compila, as referencias estao garantidas como validas.

Traits e Generics

Pergunta 4: Como funcionam os traits no Rust?

Os traits definem comportamento compartilhado entre diferentes tipos, semelhante a interfaces, mas com funcionalidades adicionais.

traits_basics.rsrust
// Defining and implementing traits

// Trait definition
trait Summary {
    // Required method (no body)
    fn summarize(&self) -> String;

    // Method with default implementation
    fn summarize_author(&self) -> String {
        String::from("(Anonymous)")
    }

    // Default method that calls a required method
    fn full_summary(&self) -> String {
        format!("By {} - {}", self.summarize_author(), self.summarize())
    }
}

// Implementation for different types
struct NewsArticle {
    headline: String,
    location: String,
    author: String,
    content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }

    fn summarize_author(&self) -> String {
        format!("@{}", self.author)
    }
}

struct Tweet {
    username: String,
    content: String,
    reply: bool,
    retweet: bool,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

// Trait bounds: constraining generics
fn notify<T: Summary>(item: &T) {
    println!("Breaking news! {}", item.summarize());
}

// Alternative syntax with where
fn notify_verbose<T>(item: &T)
where
    T: Summary,
{
    println!("Breaking news! {}", item.summarize());
}

// Multiple trait bounds
fn notify_complex<T: Summary + Clone + std::fmt::Display>(item: &T) {
    println!("{}", item);
}

// Return a type that implements a trait
fn create_summarizable() -> impl Summary {
    Tweet {
        username: String::from("rust_lang"),
        content: String::from("Rust 2026 is amazing!"),
        reply: false,
        retweet: false,
    }
}

Os traits possibilitam polimorfismo sem heranca de classes, favorecendo a composicao em detrimento da heranca.

Pergunta 5: Explicar a diferenca entre genericidade estatica e dinamica

O Rust oferece duas abordagens para polimorfismo: monomorfizacao (estatica) e objetos trait (dinamica).

static_vs_dynamic_dispatch.rsrust
// Static vs dynamic dispatch

trait Animal {
    fn speak(&self) -> String;
    fn name(&self) -> &str;
}

struct Dog { name: String }
struct Cat { name: String }

impl Animal for Dog {
    fn speak(&self) -> String { String::from("Woof!") }
    fn name(&self) -> &str { &self.name }
}

impl Animal for Cat {
    fn speak(&self) -> String { String::from("Meow!") }
    fn name(&self) -> &str { &self.name }
}

// STATIC DISPATCH (monomorphization)
// Compiler generates a version for each concrete type
fn make_speak_static<T: Animal>(animal: &T) {
    // At compile time, becomes make_speak_Dog and make_speak_Cat
    println!("{} says {}", animal.name(), animal.speak());
}

// Advantages: inlining possible, no runtime overhead
// Disadvantages: larger binary, type must be known at compile time

// DYNAMIC DISPATCH (trait objects)
// Uses a vtable to resolve methods at runtime
fn make_speak_dynamic(animal: &dyn Animal) {
    // Resolved via a pointer table (vtable) at runtime
    println!("{} says {}", animal.name(), animal.speak());
}

// Advantages: can store different types, smaller binary
// Disadvantages: indirection overhead, no inlining

fn main() {
    let dog = Dog { name: String::from("Rex") };
    let cat = Cat { name: String::from("Whiskers") };

    // Static: type is known at compile time
    make_speak_static(&dog);
    make_speak_static(&cat);

    // Dynamic: type is resolved at runtime
    make_speak_dynamic(&dog);
    make_speak_dynamic(&cat);

    // Heterogeneous collection (requires dynamic dispatch)
    let animals: Vec<Box<dyn Animal>> = vec![
        Box::new(Dog { name: String::from("Buddy") }),
        Box::new(Cat { name: String::from("Luna") }),
    ];

    for animal in animals.iter() {
        println!("{} says {}", animal.name(), animal.speak());
    }
}

// Object safety: not all traits can become trait objects
trait ObjectSafe {
    fn method(&self);
    // No Self in return type
    // No generic parameters
}

// NOT object safe (cannot be dyn NotObjectSafe)
trait NotObjectSafe {
    fn create() -> Self;  // Self in return
    fn generic<T>(&self, t: T);  // Generic
}

O dispatch estatico e preferivel para desempenho. O dispatch dinamico e util para colecoes heterogeneas e flexibilidade.

Pronto para mandar bem nas entrevistas de Rust?

Pratique com nossos simuladores interativos, flashcards e testes tecnicos.

Tratamento de Erros

Pergunta 6: Como tratar erros com Result e Option?

O Rust nao possui excecoes. O tratamento de erros e feito por meio dos tipos Result<T, E> e Option<T> com pattern matching.

error_handling.rsrust
// Idiomatic error handling in Rust

use std::fs::File;
use std::io::{self, Read};

// Option<T>: presence or absence of a value
fn find_user(id: u32) -> Option<String> {
    match id {
        1 => Some(String::from("Alice")),
        2 => Some(String::from("Bob")),
        _ => None,  // No user found
    }
}

// Result<T, E>: success or error
fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err(String::from("Division by zero"))
    } else {
        Ok(a / b)
    }
}

fn option_combinators() {
    let user = find_user(1);

    // Pattern matching
    match user {
        Some(name) => println!("Found: {}", name),
        None => println!("Not found"),
    }

    // unwrap_or: default value
    let name = find_user(99).unwrap_or(String::from("Unknown"));

    // map: transform the value if present
    let upper = find_user(1).map(|n| n.to_uppercase());

    // and_then (flatMap): chain Options
    let first_char = find_user(1).and_then(|n| n.chars().next());

    // if let: simplified pattern matching
    if let Some(name) = find_user(2) {
        println!("User 2 is {}", name);
    }
}

fn result_handling() -> Result<(), Box<dyn std::error::Error>> {
    // The ? operator propagates errors automatically
    let result = divide(10.0, 2.0)?;
    println!("Result: {}", result);

    // Equivalent to:
    // let result = match divide(10.0, 2.0) {
    //     Ok(v) => v,
    //     Err(e) => return Err(e.into()),
    // };

    Ok(())
}

// File reading with error propagation
fn read_file_contents(path: &str) -> Result<String, io::Error> {
    let mut file = File::open(path)?;  // Propagates error if failure
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(contents)
}

// Custom errors
#[derive(Debug)]
enum AppError {
    IoError(io::Error),
    ParseError(String),
    NotFound(String),
}

impl std::fmt::Display for AppError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            AppError::IoError(e) => write!(f, "IO error: {}", e),
            AppError::ParseError(s) => write!(f, "Parse error: {}", s),
            AppError::NotFound(s) => write!(f, "Not found: {}", s),
        }
    }
}

impl std::error::Error for AppError {}

// Automatic conversion with From
impl From<io::Error> for AppError {
    fn from(error: io::Error) -> Self {
        AppError::IoError(error)
    }
}

fn complex_operation() -> Result<String, AppError> {
    let contents = std::fs::read_to_string("config.txt")?;  // Auto-convert

    if contents.is_empty() {
        return Err(AppError::NotFound(String::from("Config is empty")));
    }

    Ok(contents)
}

O operador ? torna o codigo conciso enquanto exige o tratamento explicito de erros. Sem surpresas em tempo de execucao.

Evitar unwrap em producao

unwrap() e expect() causam panic se o valor for None ou Err. Devem ser reservados para prototipos ou casos onde a falha e impossivel. Em producao, a propagacao com ? ou combinadores e preferivel.

Pergunta 7: Como criar erros personalizados com thiserror?

O crate thiserror simplifica a criacao de erros personalizados ergonomicos.

custom_errors.rsrust
// Custom errors with thiserror

use thiserror::Error;

// Error definition with derive macro
#[derive(Error, Debug)]
pub enum DataStoreError {
    #[error("connection failed: {0}")]
    ConnectionFailed(String),

    #[error("query failed: {query}")]
    QueryFailed { query: String, source: std::io::Error },

    #[error("record not found: id={id}")]
    NotFound { id: u64 },

    #[error("invalid data: {0}")]
    InvalidData(#[from] serde_json::Error),

    #[error(transparent)]  // Delegates Display to source
    Other(#[from] anyhow::Error),
}

// Implementation with rich context
pub struct DataStore {
    connection_string: String,
}

impl DataStore {
    pub fn connect(conn_str: &str) -> Result<Self, DataStoreError> {
        if conn_str.is_empty() {
            return Err(DataStoreError::ConnectionFailed(
                "Empty connection string".into()
            ));
        }
        Ok(Self { connection_string: conn_str.to_string() })
    }

    pub fn get_record(&self, id: u64) -> Result<Record, DataStoreError> {
        // Query simulation
        if id == 0 {
            return Err(DataStoreError::NotFound { id });
        }
        Ok(Record { id, data: format!("Record {}", id) })
    }
}

pub struct Record {
    pub id: u64,
    pub data: String,
}

// Usage with anyhow for applications
use anyhow::{Context, Result};

fn application_code() -> Result<()> {
    let store = DataStore::connect("postgres://localhost/db")
        .context("Failed to connect to database")?;

    let record = store.get_record(42)
        .context("Failed to fetch user record")?;

    println!("Got: {}", record.data);
    Ok(())
}

// Pattern: converting errors with context
fn read_config() -> Result<Config> {
    let contents = std::fs::read_to_string("config.toml")
        .context("Failed to read config file")?;

    let config: Config = toml::from_str(&contents)
        .context("Failed to parse config file")?;

    Ok(config)
}

#[derive(Debug)]
struct Config {
    // ...
}

thiserror e ideal para bibliotecas (erros tipados), enquanto anyhow se adapta melhor a aplicacoes (maxima flexibilidade).

Smart Pointers

Pergunta 8: Explicar Box, Rc, Arc e RefCell

Os smart pointers gerenciam a memoria do heap e permitem padroes que o ownership simples nao suporta.

smart_pointers.rsrust
// Main smart pointers in Rust

use std::rc::Rc;
use std::sync::Arc;
use std::cell::RefCell;

// BOX<T>: heap allocation
// Used for: recursive types, large types, trait objects
fn box_example() {
    // Simple heap allocation
    let b = Box::new(5);
    println!("b = {}", b);

    // Recursive type (impossible without Box)
    #[derive(Debug)]
    enum List {
        Cons(i32, Box<List>),
        Nil,
    }

    let list = List::Cons(1,
        Box::new(List::Cons(2,
            Box::new(List::Cons(3,
                Box::new(List::Nil))))));
    println!("{:?}", list);
}

// RC<T>: Reference Counting (single-threaded)
// Multiple owners for the same data
fn rc_example() {
    let data = Rc::new(vec![1, 2, 3]);

    // Clone increments the reference counter
    let data_clone1 = Rc::clone(&data);  // count = 2
    let data_clone2 = Rc::clone(&data);  // count = 3

    println!("Reference count: {}", Rc::strong_count(&data));  // 3

    // Each clone can read the data
    println!("data_clone1: {:?}", data_clone1);

    // Data is freed when the last Rc is dropped
}

// ARC<T>: Atomic Reference Counting (thread-safe)
// Like Rc but usable across threads
fn arc_example() {
    use std::thread;

    let data = Arc::new(vec![1, 2, 3, 4, 5]);
    let mut handles = vec![];

    for i in 0..3 {
        let data_clone = Arc::clone(&data);
        let handle = thread::spawn(move || {
            // Each thread has its own Arc
            println!("Thread {}: {:?}", i, data_clone);
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }
}

// REFCELL<T>: Interior Mutability
// Allows mutation even with an immutable reference
fn refcell_example() {
    let data = RefCell::new(5);

    // borrow() returns an immutable reference
    println!("Value: {}", *data.borrow());

    // borrow_mut() returns a mutable reference
    *data.borrow_mut() += 1;
    println!("After mutation: {}", *data.borrow());

    // Borrowing rules are checked at RUNTIME
    // Panics if rules are violated
    // let r1 = data.borrow();
    // let r2 = data.borrow_mut();  // PANIC: already borrowed
}

// Common combination: Rc<RefCell<T>>
// Multiple owners with possible mutation
fn rc_refcell_example() {
    #[derive(Debug)]
    struct Node {
        value: i32,
        children: Vec<Rc<RefCell<Node>>>,
    }

    let node1 = Rc::new(RefCell::new(Node {
        value: 1,
        children: vec![],
    }));

    let node2 = Rc::new(RefCell::new(Node {
        value: 2,
        children: vec![Rc::clone(&node1)],  // node1 is child of node2
    }));

    // Modify node1 from anywhere
    node1.borrow_mut().value = 10;

    println!("node2 child value: {}",
        node2.borrow().children[0].borrow().value);  // 10
}

// For threads: Arc<Mutex<T>> or Arc<RwLock<T>>
fn arc_mutex_example() {
    use std::sync::Mutex;
    use std::thread;

    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!("Final count: {}", *counter.lock().unwrap());  // 10
}

A escolha do smart pointer correto depende do contexto: Box para heap simples, Rc/Arc para compartilhamento, RefCell/Mutex para mutabilidade interior.

Concorrencia

Pergunta 9: Como o Rust garante seguranca entre threads?

O sistema de tipos do Rust previne data races em tempo de compilacao por meio dos traits Send e Sync.

thread_safety.rsrust
// Concurrent safety guarantees

use std::thread;
use std::sync::{Arc, Mutex, mpsc};

// SEND: a type can be transferred to another thread
// SYNC: a type can be shared between threads via references

// Most types are Send and Sync automatically
// Exceptions: Rc (not Send/Sync), RefCell (not Sync), raw pointers

fn send_example() {
    let data = vec![1, 2, 3];

    // Vec is Send, so it can be moved to another thread
    let handle = thread::spawn(move || {
        println!("Data in thread: {:?}", data);
    });

    handle.join().unwrap();
}

// The compiler prevents concurrency errors
fn compile_time_safety() {
    // This would NOT compile:
    // let data = std::rc::Rc::new(5);
    // thread::spawn(move || {
    //     println!("{}", data);  // ERROR: Rc is not Send
    // });

    // Solution: use Arc
    let data = Arc::new(5);
    let data_clone = Arc::clone(&data);

    thread::spawn(move || {
        println!("{}", data_clone);  // OK: Arc is Send
    });
}

// Mutex for thread-safe shared mutation
fn mutex_pattern() {
    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 || {
            // lock() blocks until exclusive access is obtained
            let mut num = counter.lock().unwrap();
            *num += 1;
            // MutexGuard is dropped here, releasing the lock
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Result: {}", *counter.lock().unwrap());
}

// RwLock for multiple reads / exclusive write
fn rwlock_example() {
    use std::sync::RwLock;

    let data = Arc::new(RwLock::new(vec![1, 2, 3]));
    let mut handles = vec![];

    // Multiple simultaneous readers
    for i in 0..3 {
        let data = Arc::clone(&data);
        handles.push(thread::spawn(move || {
            let read = data.read().unwrap();
            println!("Reader {}: {:?}", i, *read);
        }));
    }

    // Only one writer at a time
    {
        let data = Arc::clone(&data);
        handles.push(thread::spawn(move || {
            let mut write = data.write().unwrap();
            write.push(4);
            println!("Writer added 4");
        }));
    }

    for handle in handles {
        handle.join().unwrap();
    }
}

// Channels for inter-thread communication
fn channel_example() {
    let (tx, rx) = mpsc::channel();  // Multi-producer, single-consumer

    // Clone the sender for multiple producers
    let tx1 = tx.clone();
    thread::spawn(move || {
        tx1.send("from thread 1").unwrap();
    });

    thread::spawn(move || {
        tx.send("from thread 2").unwrap();
    });

    // Receive messages
    for received in rx {
        println!("Got: {}", received);
    }
}

"Fearless concurrency": se o codigo compila, nao existem data races. O compilador e a primeira linha de defesa.

Mutex Poisoning

Se uma thread entra em panic enquanto possui um Mutex, este fica "envenenado". Chamadas posteriores a lock() retornam um erro que pode ser recuperado com into_inner().

Pergunta 10: Como funciona o async/await no Rust?

O async no Rust e baseado em Futures de custo zero, sem um runtime embutido na linguagem.

async_await.rsrust
// Asynchronous programming in Rust

use tokio::time::{sleep, Duration};

// async fn returns a Future that must be executed
async fn fetch_data(url: &str) -> Result<String, reqwest::Error> {
    // await suspends execution without blocking the thread
    let response = reqwest::get(url).await?;
    let body = response.text().await?;
    Ok(body)
}

// Futures are lazy: nothing executes without await or poll
async fn lazy_example() {
    let future = async {
        println!("This won't print yet");
    };
    // Nothing happened

    future.await;  // Now it executes
}

// Parallel execution of futures
async fn parallel_execution() {
    // join! executes multiple futures in parallel
    let (result1, result2) = tokio::join!(
        fetch_data("https://api.example.com/1"),
        fetch_data("https://api.example.com/2"),
    );

    println!("Results: {:?}, {:?}", result1, result2);
}

// select! for the first completed future
async fn race_example() {
    tokio::select! {
        result = fetch_data("https://api1.example.com") => {
            println!("API 1 responded first: {:?}", result);
        }
        result = fetch_data("https://api2.example.com") => {
            println!("API 2 responded first: {:?}", result);
        }
        _ = sleep(Duration::from_secs(5)) => {
            println!("Timeout!");
        }
    }
}

// Streams: asynchronous iterators
use tokio_stream::StreamExt;

async fn stream_example() {
    let mut stream = tokio_stream::iter(vec![1, 2, 3, 4, 5]);

    while let Some(value) = stream.next().await {
        println!("Got: {}", value);
    }
}

// Spawn for background tasks
async fn spawn_tasks() {
    let handle = tokio::spawn(async {
        sleep(Duration::from_secs(1)).await;
        "Task completed"
    });

    println!("Task spawned, doing other work...");

    let result = handle.await.unwrap();
    println!("Result: {}", result);
}

// Entry point with tokio
#[tokio::main]
async fn main() {
    // The tokio runtime executes futures
    parallel_execution().await;
}

// Alternative: multi-threaded or single-threaded runtime
#[tokio::main(flavor = "current_thread")]
async fn main_single_thread() {
    // Everything runs on a single thread
}

#[tokio::main(flavor = "multi_thread", worker_threads = 4)]
async fn main_multi_thread() {
    // Pool of 4 worker threads
}

O async do Rust segue o principio "bring your own runtime": tokio, async-std ou smol. Essa flexibilidade permite otimizacoes especificas conforme o caso de uso.

Pronto para mandar bem nas entrevistas de Rust?

Pratique com nossos simuladores interativos, flashcards e testes tecnicos.

Padroes Avancados

Pergunta 11: Explicar o padrao Builder no Rust

O padrao Builder e idiomatico no Rust para construir estruturas complexas com muitos campos opcionais.

builder_pattern.rsrust
// Idiomatic Builder pattern in Rust

#[derive(Debug, Clone)]
pub struct Server {
    host: String,
    port: u16,
    max_connections: usize,
    timeout_seconds: u64,
    tls_enabled: bool,
    tls_cert_path: Option<String>,
}

// Builder with consuming approach (ownership)
#[derive(Default)]
pub struct ServerBuilder {
    host: String,
    port: u16,
    max_connections: usize,
    timeout_seconds: u64,
    tls_enabled: bool,
    tls_cert_path: Option<String>,
}

impl ServerBuilder {
    pub fn new() -> Self {
        Self {
            host: String::from("localhost"),
            port: 8080,
            max_connections: 100,
            timeout_seconds: 30,
            tls_enabled: false,
            tls_cert_path: None,
        }
    }

    // Each method takes self and returns Self for chaining
    pub fn host(mut self, host: impl Into<String>) -> Self {
        self.host = host.into();
        self
    }

    pub fn port(mut self, port: u16) -> Self {
        self.port = port;
        self
    }

    pub fn max_connections(mut self, max: usize) -> Self {
        self.max_connections = max;
        self
    }

    pub fn timeout(mut self, seconds: u64) -> Self {
        self.timeout_seconds = seconds;
        self
    }

    pub fn enable_tls(mut self, cert_path: impl Into<String>) -> Self {
        self.tls_enabled = true;
        self.tls_cert_path = Some(cert_path.into());
        self
    }

    // build() consumes the builder and creates the final structure
    pub fn build(self) -> Result<Server, String> {
        if self.tls_enabled && self.tls_cert_path.is_none() {
            return Err("TLS enabled but no certificate path provided".into());
        }

        Ok(Server {
            host: self.host,
            port: self.port,
            max_connections: self.max_connections,
            timeout_seconds: self.timeout_seconds,
            tls_enabled: self.tls_enabled,
            tls_cert_path: self.tls_cert_path,
        })
    }
}

// Fluent usage
fn create_server() -> Result<Server, String> {
    ServerBuilder::new()
        .host("0.0.0.0")
        .port(443)
        .max_connections(1000)
        .timeout(60)
        .enable_tls("/etc/ssl/cert.pem")
        .build()
}

// Alternative with derive macro (typed-builder crate)
// #[derive(TypedBuilder)]
// pub struct Config {
//     #[builder(default = "localhost".to_string())]
//     host: String,
//     #[builder(default = 8080)]
//     port: u16,
// }

// Pattern with type-level validation (typestate pattern)
pub struct Unvalidated;
pub struct Validated;

pub struct Request<State = Unvalidated> {
    url: String,
    method: String,
    headers: Vec<(String, String)>,
    _state: std::marker::PhantomData<State>,
}

impl Request<Unvalidated> {
    pub fn new(url: &str) -> Self {
        Self {
            url: url.to_string(),
            method: "GET".to_string(),
            headers: vec![],
            _state: std::marker::PhantomData,
        }
    }

    pub fn method(mut self, method: &str) -> Self {
        self.method = method.to_string();
        self
    }

    // validate() changes the state type
    pub fn validate(self) -> Result<Request<Validated>, String> {
        if self.url.is_empty() {
            return Err("URL cannot be empty".into());
        }
        Ok(Request {
            url: self.url,
            method: self.method,
            headers: self.headers,
            _state: std::marker::PhantomData,
        })
    }
}

impl Request<Validated> {
    // send() is only available on validated requests
    pub async fn send(self) -> Result<Response, reqwest::Error> {
        // Implementation...
        todo!()
    }
}

struct Response;

O padrao typestate garante em tempo de compilacao que certas operacoes so podem ser chamadas no estado correto.

Pergunta 12: Como implementar um trait para tipos externos?

A "regra do orfao" impede a implementacao de um trait externo para um tipo externo, mas existem solucoes.

newtype_pattern.rsrust
// The Newtype pattern to work around the orphan rule

use std::fmt;

// ORPHAN RULE: cannot implement Display (std) for Vec (std)
// impl fmt::Display for Vec<i32> { ... }  // ERROR

// SOLUTION 1: Newtype wrapper
struct Wrapper(Vec<String>);

impl fmt::Display for Wrapper {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "[{}]", self.0.join(", "))
    }
}

fn newtype_example() {
    let w = Wrapper(vec![
        String::from("hello"),
        String::from("world"),
    ]);
    println!("{}", w);  // [hello, world]
}

// Transparent access with Deref
use std::ops::Deref;

impl Deref for Wrapper {
    type Target = Vec<String>;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

fn deref_example() {
    let w = Wrapper(vec![String::from("test")]);
    println!("Length: {}", w.len());  // Calls Vec::len via Deref
}

// SOLUTION 2: Extension trait (to add methods)
trait VecExt<T> {
    fn first_or_default(&self) -> Option<&T>;
}

impl<T> VecExt<T> for Vec<T> {
    fn first_or_default(&self) -> Option<&T> {
        self.first()
    }
}

fn extension_trait_example() {
    let v = vec![1, 2, 3];
    println!("First: {:?}", v.first_or_default());
}

// Newtype with domain semantics
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Email(String);

impl Email {
    pub fn new(email: &str) -> Result<Self, &'static str> {
        if email.contains('@') && email.contains('.') {
            Ok(Self(email.to_string()))
        } else {
            Err("Invalid email format")
        }
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl fmt::Display for Email {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct UserId(u64);

impl UserId {
    pub fn new(id: u64) -> Self {
        Self(id)
    }
}

// Newtypes add type safety without runtime overhead
fn process_user(id: UserId, email: Email) {
    println!("Processing user {} with email {}", id.0, email);
}

fn type_safety_example() {
    let id = UserId::new(42);
    let email = Email::new("user@example.com").unwrap();

    process_user(id, email);

    // This would not compile:
    // process_user(email, id);  // Types reversed
    // process_user(UserId::new(42), "string");  // String instead of Email
}

Os newtypes nao possuem custo em tempo de execucao gracas a garantia de representacao de memoria identica.

Pergunta 13: Como usar macros procedurais?

As macros procedurais permitem gerar codigo em tempo de compilacao, como derives personalizados.

procedural_macros.rsrust
// Understanding procedural macros

// Proc macros are defined in a separate crate with proc-macro = true

// Crate: my_derive (Cargo.toml: proc-macro = true)
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};

// DERIVE MACRO: #[derive(MyTrait)]
#[proc_macro_derive(MyDebug)]
pub fn my_debug_derive(input: TokenStream) -> TokenStream {
    // Parse input as a type definition
    let input = parse_macro_input!(input as DeriveInput);
    let name = input.ident;

    // Generate implementation code
    let expanded = quote! {
        impl std::fmt::Debug for #name {
            fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
                write!(f, stringify!(#name))
            }
        }
    };

    TokenStream::from(expanded)
}

// ATTRIBUTE MACRO: #[my_attribute]
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
    // attr contains the attribute arguments
    // item contains the annotated element (function, struct, etc.)

    let method_path = attr.to_string();  // "GET, /users"
    let input = parse_macro_input!(item as syn::ItemFn);
    let fn_name = &input.sig.ident;

    let expanded = quote! {
        #input

        // Additionally generated code
        fn register_#fn_name() {
            println!("Registered route: {}", #method_path);
        }
    };

    TokenStream::from(expanded)
}

// FUNCTION-LIKE MACRO: my_macro!(...)
#[proc_macro]
pub fn make_answer(_input: TokenStream) -> TokenStream {
    "fn answer() -> u32 { 42 }".parse().unwrap()
}

// --- Usage in client code ---

// Derive macro
#[derive(MyDebug)]
struct Point {
    x: i32,
    y: i32,
}

// Attribute macro
#[route("GET", "/users")]
fn get_users() -> Vec<User> {
    vec![]
}

// Function-like macro
make_answer!();  // Generates fn answer() -> u32 { 42 }

fn main() {
    let p = Point { x: 1, y: 2 };
    println!("{:?}", p);  // Uses our MyDebug

    println!("Answer: {}", answer());  // 42
}

struct User;

As macros procedurais sao poderosas para codigo repetitivo: serializacao, roteamento web, validacao, entre outros.

Seguranca de Memoria e Unsafe

Pergunta 14: Quando e como usar unsafe?

O bloco unsafe permite contornar certas verificacoes do compilador para codigo de baixo nivel.

unsafe_rust.rsrust
// Understanding unsafe and its guarantees

// The 5 superpowers of unsafe:
// 1. Dereference raw pointers
// 2. Call unsafe functions
// 3. Access/modify mutable static variables
// 4. Implement unsafe traits
// 5. Access union fields

// RAW POINTERS
fn raw_pointers() {
    let mut num = 5;

    // Creating raw pointers is safe
    let r1 = &num as *const i32;
    let r2 = &mut num as *mut i32;

    // Dereferencing requires unsafe
    unsafe {
        println!("r1 is: {}", *r1);
        *r2 = 10;
        println!("r2 is: {}", *r2);
    }
}

// UNSAFE FUNCTION
// The function guarantees safety IF preconditions are met
unsafe fn dangerous() {
    // Code that assumes the caller verified invariants
}

fn call_dangerous() {
    // Must be in an unsafe block
    unsafe {
        dangerous();
    }
}

// SAFE ABSTRACTION over unsafe code
fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    let len = values.len();
    let ptr = values.as_mut_ptr();

    assert!(mid <= len);  // Runtime check

    unsafe {
        // We know the two slices don't overlap
        (
            std::slice::from_raw_parts_mut(ptr, mid),
            std::slice::from_raw_parts_mut(ptr.add(mid), len - mid),
        )
    }
}

// FFI: calling C code
extern "C" {
    fn abs(input: i32) -> i32;
}

fn call_c_function() {
    unsafe {
        println!("Absolute value: {}", abs(-3));
    }
}

// Export a function for C
#[no_mangle]
pub extern "C" fn call_from_c() {
    println!("Called from C!");
}

// MUTABLE STATIC
static mut COUNTER: u32 = 0;

fn increment_counter() {
    unsafe {
        COUNTER += 1;
        println!("COUNTER: {}", COUNTER);
    }
}

// UNSAFE TRAIT
unsafe trait Dangerous {
    // Implementers guarantee invariants
}

unsafe impl Dangerous for i32 {
    // Implementer asserts respecting the trait's invariants
}

// Practical example: structure with internal pointer
pub struct MyVec<T> {
    ptr: *mut T,
    len: usize,
    capacity: usize,
}

impl<T> MyVec<T> {
    pub fn new() -> Self {
        Self {
            ptr: std::ptr::null_mut(),
            len: 0,
            capacity: 0,
        }
    }

    pub fn push(&mut self, value: T) {
        if self.len == self.capacity {
            self.grow();
        }

        unsafe {
            std::ptr::write(self.ptr.add(self.len), value);
        }
        self.len += 1;
    }

    fn grow(&mut self) {
        // Unsafe allocation/reallocation...
    }
}

impl<T> Drop for MyVec<T> {
    fn drop(&mut self) {
        unsafe {
            // Properly free memory
            for i in 0..self.len {
                std::ptr::drop_in_place(self.ptr.add(i));
            }
            if self.capacity > 0 {
                let layout = std::alloc::Layout::array::<T>(self.capacity).unwrap();
                std::alloc::dealloc(self.ptr as *mut u8, layout);
            }
        }
    }
}
Regra de ouro do unsafe

Minimizar a superficie de codigo unsafe. Encapsular o codigo unsafe em abstracoes seguras que garantam invariantes. O codigo unsafe nunca deve corromper a memoria segura ao redor.

Pergunta 15: Como funciona o borrow checker?

O borrow checker e o nucleo do compilador do Rust que verifica as regras de ownership e borrowing.

borrow_checker.rsrust
// Understanding how the borrow checker works

fn borrow_checker_basics() {
    let mut v = vec![1, 2, 3];

    // RULE 1: Either multiple immutable references or one mutable
    let r1 = &v;
    let r2 = &v;
    println!("{:?} {:?}", r1, r2);  // OK: multiple immutable references

    // From here, r1 and r2 are no longer used (NLL)

    let r3 = &mut v;  // OK thanks to Non-Lexical Lifetimes
    r3.push(4);
}

// The borrow checker tracks lifetimes
fn lifetime_tracking() {
    let mut data = String::from("hello");

    let slice = &data[..];  // Immutable borrow starts
    // data.push_str(" world");  // ERROR: cannot mutate during borrow
    println!("{}", slice);  // Last use of slice

    data.push_str(" world");  // OK: borrow ended
}

// Common problems and solutions
mod common_patterns {
    // Problem: borrowing two mutable fields
    struct Data {
        field1: Vec<i32>,
        field2: Vec<i32>,
    }

    fn problem(data: &mut Data) {
        // This sometimes doesn't compile directly:
        // let f1 = &mut data.field1;
        // let f2 = &mut data.field2;

        // Solution: destructuring
        let Data { field1, field2 } = data;
        field1.push(1);
        field2.push(2);
    }

    // Problem: iterate and modify
    fn iterate_and_modify() {
        let mut v = vec![1, 2, 3, 4, 5];

        // Does not compile:
        // for &x in &v {
        //     if x % 2 == 0 {
        //         v.push(x * 2);  // ERROR: borrowed by iterator
        //     }
        // }

        // Solution 1: collect indices first
        let to_add: Vec<i32> = v.iter()
            .filter(|&&x| x % 2 == 0)
            .map(|&x| x * 2)
            .collect();
        v.extend(to_add);

        // Solution 2: use explicit indices
        let len = v.len();
        for i in 0..len {
            if v[i] % 2 == 0 {
                let new_val = v[i] * 2;
                v.push(new_val);
            }
        }
    }

    // Problem: self-referential struct
    // struct SelfRef {
    //     data: String,
    //     slice: &str,  // Reference to data - IMPOSSIBLE
    // }

    // Solution: use indices or crates like ouroboros
    struct SafeSelfRef {
        data: String,
        slice_start: usize,
        slice_end: usize,
    }

    impl SafeSelfRef {
        fn get_slice(&self) -> &str {
            &self.data[self.slice_start..self.slice_end]
        }
    }
}

// Patterns to work around limitations
mod workarounds {
    use std::cell::RefCell;

    // Interior mutability when borrow checker is too restrictive
    struct Graph {
        nodes: RefCell<Vec<Node>>,
    }

    struct Node {
        value: i32,
    }

    impl Graph {
        fn add_node(&self, value: i32) {
            // Mutation possible despite &self
            self.nodes.borrow_mut().push(Node { value });
        }

        fn get_node(&self, index: usize) -> Option<i32> {
            self.nodes.borrow().get(index).map(|n| n.value)
        }
    }
}

O borrow checker pode parecer restritivo no inicio, mas essas restricoes eliminam categorias inteiras de bugs presentes em outras linguagens.

Conclusao

As entrevistas sobre Rust avaliam a compreensao profunda do sistema de ownership, as garantias de seguranca de memoria e a capacidade de escrever codigo concorrente sem data races. Dominar esses conceitos diferencia os desenvolvedores que conseguem aproveitar as vantagens unicas do Rust.

Lista de verificacao para a preparacao

  • Compreender o ownership, borrowing e as tres regras fundamentais
  • Saber quando e como anotar lifetimes
  • Dominar os traits e a diferenca entre dispatch estatico e dinamico
  • Tratar erros de forma idiomatica com Result e Option
  • Escolher o smart pointer adequado conforme o contexto
  • Escrever codigo concorrente com Arc, Mutex e canais
  • Compreender async/await e runtimes como tokio
  • Saber quando e como usar unsafe de forma segura

Comece a praticar!

Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.

A preparacao para entrevistas sobre Rust exige pratica com os conceitos unicos de ownership da linguagem. Exercicios no Exercism, projetos pessoais e contribuicoes para o ecossistema Rust consolidam esse conhecimento para as entrevistas tecnicas mais exigentes.

Tags

#rust
#interview
#systems programming
#ownership
#technical interview

Compartilhar

Artigos relacionados