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 implementeReadpode 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 paraResult<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 comoNotFound,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/Tipo | Descriçã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
BufReadereBufWritersempre que fizer múltiplas operações pequenas de leitura ou escrita. Sem buffering, cada chamada aread()ouwrite()gera uma syscall ao sistema operacional. - Prefira
io::copyem 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 emBufWriterantes de descartar o escritor, para garantir que os dados foram enviados.
Veja também
- Read e Write Traits — detalhamento completo das traits Read e Write
- BufReader e BufWriter — I/O bufferizado em profundidade
- File e OpenOptions — abertura e criação de arquivos
- stdin, stdout e stderr — I/O padrão do processo
- Ler Arquivo em Rust — receita prática de leitura de arquivos
- Tratar Erros em Rust — padrões gerais de tratamento de erros
- Documentação oficial:
std::io