Módulo std::io em Rust

Referência completa do módulo std::io em Rust: traits Read, Write, BufRead, io::Result, io::Error, ErrorKind e funções utilitárias com exemplos.

Módulo std::io em Rust

O módulo std::io é o coração do sistema de entrada e saída do Rust. Ele define as traits fundamentais Read, Write e BufRead, o tipo de resultado io::Result<T>, o tipo de erro io::Error com suas categorias via ErrorKind, além de funções utilitárias como copy, sink, repeat e empty. Todo código que lida com I/O em Rust passa, direta ou indiretamente, por este módulo.

Visão geral e tipos-chave

O std::io organiza o I/O do Rust em torno de abstrações baseadas em traits. Em vez de trabalhar com tipos concretos, você programa contra interfaces — o que permite que o mesmo código funcione com arquivos, sockets de rede, buffers em memória e qualquer outro tipo que implemente as traits apropriadas.

Traits fundamentais

  • Read — leitura de bytes de uma fonte. Qualquer tipo que implemente Read pode fornecer dados byte a byte ou em blocos.
  • Write — escrita de bytes em um destino. Suporta escrita parcial e flush de buffers internos.
  • BufRead — leitura bufferizada com acesso ao buffer interno. Permite leitura por linhas e preenchimento eficiente.
  • Seek — movimentação do cursor de leitura/escrita dentro de um fluxo.

Tipos de erro

  • io::Result<T> — alias para Result<T, io::Error>, usado em praticamente toda operação de I/O.
  • io::Error — tipo de erro rico com categoria (ErrorKind), mensagem e erro de origem opcional.
  • io::ErrorKind — enumeração com categorias como NotFound, PermissionDenied, WouldBlock, UnexpectedEof, etc.

Padrões comuns com código

Usando io::Result e o operador ?

O padrão mais idiomático para lidar com erros de I/O é retornar io::Result e propagar erros com ?:

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

fn ler_primeiros_bytes(caminho: &str, n: usize) -> io::Result<Vec<u8>> {
    let mut arquivo = File::open(caminho)?;
    let mut buffer = vec![0u8; n];
    let bytes_lidos = arquivo.read(&mut buffer)?;
    buffer.truncate(bytes_lidos);
    Ok(buffer)
}

fn main() {
    match ler_primeiros_bytes("dados.bin", 256) {
        Ok(bytes) => println!("Lidos {} bytes: {:?}", bytes.len(), &bytes[..8.min(bytes.len())]),
        Err(e) => eprintln!("Erro de I/O: {} (tipo: {:?})", e, e.kind()),
    }
}

Criando erros customizados com io::Error

Você pode criar erros de I/O personalizados para integrar com o sistema de erros padrão:

use std::io;

fn validar_e_processar(dados: &[u8]) -> io::Result<String> {
    if dados.is_empty() {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            "Dados vazios não são permitidos",
        ));
    }

    if dados.len() > 10_000_000 {
        return Err(io::Error::new(
            io::ErrorKind::InvalidInput,
            format!("Dados muito grandes: {} bytes (máximo: 10MB)", dados.len()),
        ));
    }

    String::from_utf8(dados.to_vec()).map_err(|e| {
        io::Error::new(io::ErrorKind::InvalidData, e)
    })
}

Classificando erros com ErrorKind

Use ErrorKind para tomar decisões com base na categoria do erro:

use std::fs;
use std::io;

fn ler_ou_criar_padrao(caminho: &str, padrao: &str) -> io::Result<String> {
    match fs::read_to_string(caminho) {
        Ok(conteudo) => Ok(conteudo),
        Err(e) if e.kind() == io::ErrorKind::NotFound => {
            // Arquivo não existe — criar com valor padrão
            fs::write(caminho, padrao)?;
            Ok(padrao.to_string())
        }
        Err(e) if e.kind() == io::ErrorKind::PermissionDenied => {
            eprintln!("Sem permissão para ler: {}", caminho);
            Err(e)
        }
        Err(e) => Err(e), // outros erros propagados
    }
}

fn main() -> io::Result<()> {
    let config = ler_ou_criar_padrao("config.toml", "[padrao]\nchave = \"valor\"\n")?;
    println!("Configuração:\n{}", config);
    Ok(())
}

Tabela de funções e tipos principais

Função/TipoDescrição
io::copy(&mut reader, &mut writer)Copia todos os bytes de um Read para um Write
io::empty()Retorna um Read que sempre retorna 0 bytes (EOF)
io::repeat(byte)Retorna um Read infinito que repete o byte dado
io::sink()Retorna um Write que descarta todos os bytes (como /dev/null)
io::stdin()Retorna um handle para a entrada padrão
io::stdout()Retorna um handle para a saída padrão
io::stderr()Retorna um handle para a saída de erro
io::Error::new(kind, msg)Cria um novo erro com categoria e mensagem
io::Error::from_raw_os_error(code)Cria um erro a partir de código de erro do SO
io::Error::last_os_error()Retorna o último erro do sistema operacional
io::Read (trait)Trait para fontes de dados
io::Write (trait)Trait para destinos de dados
io::BufRead (trait)Trait para leitura bufferizada
io::Seek (trait)Trait para busca por posição

Exemplos práticos

Exemplo 1: Copiar dados entre fontes com io::copy

use std::fs::File;
use std::io;

fn copiar_arquivo(origem: &str, destino: &str) -> io::Result<u64> {
    let mut leitor = File::open(origem)?;
    let mut escritor = File::create(destino)?;

    let bytes_copiados = io::copy(&mut leitor, &mut escritor)?;
    println!("Copiados {} bytes de '{}' para '{}'", bytes_copiados, origem, destino);
    Ok(bytes_copiados)
}

fn main() -> io::Result<()> {
    copiar_arquivo("original.dat", "copia.dat")?;
    Ok(())
}

Exemplo 2: Usando sink, empty e repeat para testes

use std::io::{self, Read, Write};

fn main() -> io::Result<()> {
    // io::empty() — simula uma fonte vazia (útil em testes)
    let mut vazio = io::empty();
    let mut buf = [0u8; 10];
    let n = vazio.read(&mut buf)?;
    assert_eq!(n, 0);
    println!("empty() retornou {} bytes", n);

    // io::repeat() — gera bytes infinitamente (útil para benchmarks)
    let mut repetidor = io::repeat(0xAB).take(1024);
    let mut dados = Vec::new();
    repetidor.read_to_end(&mut dados)?;
    assert_eq!(dados.len(), 1024);
    assert!(dados.iter().all(|&b| b == 0xAB));
    println!("repeat(0xAB) gerou {} bytes", dados.len());

    // io::sink() — descarta toda escrita (útil para medir velocidade de serialização)
    let mut ralo = io::sink();
    let bytes_escritos = ralo.write(b"Estes dados serao descartados")?;
    println!("sink() aceitou {} bytes e descartou", bytes_escritos);

    Ok(())
}

Exemplo 3: Encadeando leitores com chain

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

fn main() -> io::Result<()> {
    let cabecalho = io::Cursor::new(b"--- CABECALHO ---\n");
    let corpo = io::Cursor::new(b"Conteudo do corpo do documento.\n");
    let rodape = io::Cursor::new(b"--- RODAPE ---\n");

    // Encadear três fontes em uma só
    let mut combinado = cabecalho.chain(corpo).chain(rodape);
    let mut resultado = String::new();
    combinado.read_to_string(&mut resultado)?;

    println!("{}", resultado);
    Ok(())
}

Exemplo 4: Processador de fluxo genérico

use std::io::{self, BufRead, BufReader, Write};

fn processar_fluxo<R: io::Read, W: io::Write>(
    entrada: R,
    mut saida: W,
    filtro: &str,
) -> io::Result<usize> {
    let leitor = BufReader::new(entrada);
    let mut contagem = 0;

    for linha in leitor.lines() {
        let linha = linha?;
        if linha.contains(filtro) {
            writeln!(saida, "{}", linha)?;
            contagem += 1;
        }
    }

    saida.flush()?;
    Ok(contagem)
}

fn main() -> io::Result<()> {
    // Funciona com qualquer Read/Write: arquivos, stdin/stdout, buffers...
    let dados = b"INFO: servidor iniciado\nERROR: falha na conexao\nINFO: requisicao recebida\nERROR: timeout\n";
    let entrada = io::Cursor::new(dados);
    let mut saida = Vec::new();

    let erros = processar_fluxo(entrada, &mut saida, "ERROR")?;
    println!("Encontrados {} erros:", erros);
    println!("{}", String::from_utf8_lossy(&saida));

    Ok(())
}

Exemplo 5: Medir desempenho de I/O com sink

use std::io::{self, Write};
use std::time::Instant;

fn benchmark_serializacao(dados: &[u8], repeticoes: usize) -> io::Result<()> {
    let mut ralo = io::sink();
    let inicio = Instant::now();

    for _ in 0..repeticoes {
        ralo.write_all(dados)?;
    }
    ralo.flush()?;

    let duracao = inicio.elapsed();
    let total_bytes = dados.len() * repeticoes;
    let mb_por_seg = (total_bytes as f64 / 1_000_000.0) / duracao.as_secs_f64();

    println!(
        "Escreveu {} MB em {:?} ({:.1} MB/s)",
        total_bytes / 1_000_000,
        duracao,
        mb_por_seg,
    );

    Ok(())
}

fn main() -> io::Result<()> {
    let dados = vec![42u8; 4096]; // blocos de 4KB
    benchmark_serializacao(&dados, 100_000)?;
    Ok(())
}

Padrões de tratamento de erro para I/O

Padrão 1: Retry em erros temporários

use std::io::{self, Read};
use std::thread;
use std::time::Duration;

fn ler_com_retry<R: Read>(leitor: &mut R, buf: &mut [u8], tentativas: usize) -> io::Result<usize> {
    for i in 0..tentativas {
        match leitor.read(buf) {
            Ok(n) => return Ok(n),
            Err(e) if e.kind() == io::ErrorKind::Interrupted => {
                continue; // interrupção do sistema — tentar novamente
            }
            Err(e) if e.kind() == io::ErrorKind::WouldBlock && i < tentativas - 1 => {
                thread::sleep(Duration::from_millis(10 * (i as u64 + 1)));
                continue;
            }
            Err(e) => return Err(e),
        }
    }

    Err(io::Error::new(
        io::ErrorKind::TimedOut,
        format!("Falha após {} tentativas", tentativas),
    ))
}

Padrão 2: Converter erros de outras crates

use std::io;
use std::num::ParseIntError;

fn ler_e_somar(caminho: &str) -> io::Result<i64> {
    let conteudo = std::fs::read_to_string(caminho)?;
    let mut soma: i64 = 0;

    for (num_linha, linha) in conteudo.lines().enumerate() {
        let valor: i64 = linha.trim().parse().map_err(|e: ParseIntError| {
            io::Error::new(
                io::ErrorKind::InvalidData,
                format!("Linha {}: {} — '{}'", num_linha + 1, e, linha.trim()),
            )
        })?;
        soma += valor;
    }

    Ok(soma)
}

Dicas de desempenho

  • Use BufReader e BufWriter sempre que fizer múltiplas operações pequenas de leitura ou escrita. Sem buffering, cada chamada a read() ou write() gera uma syscall ao sistema operacional.
  • Prefira io::copy em vez de ler tudo para a memória quando precisar transferir dados entre um leitor e um escritor.
  • Use take() para limitar leituras — evita consumo excessivo de memória em fontes potencialmente grandes.
  • Chame flush() explicitamente em BufWriter antes de descartar o escritor, para garantir que os dados foram enviados.

Veja também