Errdefer em Zig — O que é e Como Usar
Definição
O errdefer em Zig é uma variante do defer que executa o código agendado apenas quando o escopo é abandonado por causa de um erro. Se a função retorna com sucesso, o errdefer é ignorado. Esse mecanismo é fundamental para construir funções que alocam múltiplos recursos e precisam desfazer alocações parciais em caso de falha.
Por que Errdefer Importa
- Alocação parcial segura: Quando uma função aloca vários recursos em sequência,
errdefergarante que os já alocados sejam liberados se um dos passos falhar. - Evita vazamentos de memória: Sem
errdefer, erros no meio de uma função podem deixar recursos sem liberação. - Código mais legível: A lógica de limpeza fica junto da alocação, não em blocos de tratamento de erro distantes.
- Complementa o
defer: Enquantodeferé para limpeza incondicional,errdeferé para reverter operações.
Exemplo Prático
Padrão de Inicialização Segura
const std = @import("std");
const Servidor = struct {
socket: std.posix.socket_t,
buffer: []u8,
log: std.fs.File,
pub fn init(allocator: std.mem.Allocator) !Servidor {
// Passo 1: abrir socket
const socket = try std.posix.socket(std.posix.AF.INET, std.posix.SOCK.STREAM, 0);
errdefer std.posix.close(socket); // Fecha se passos seguintes falharem
// Passo 2: alocar buffer
const buffer = try allocator.alloc(u8, 4096);
errdefer allocator.free(buffer); // Libera se passo 3 falhar
// Passo 3: abrir arquivo de log
const log = try std.fs.cwd().createFile("servidor.log", .{});
// Sem errdefer aqui — se chegamos aqui, tudo deu certo
return Servidor{
.socket = socket,
.buffer = buffer,
.log = log,
};
}
pub fn deinit(self: *Servidor, allocator: std.mem.Allocator) void {
self.log.close();
allocator.free(self.buffer);
std.posix.close(self.socket);
}
};
Se createFile falhar, o buffer é liberado e o socket é fechado automaticamente. Se alloc falhar, apenas o socket é fechado. Cada errdefer protege contra falhas nos passos subsequentes.
Errdefer com Captura de Erro
A partir de versões recentes do Zig, errdefer pode capturar o erro:
fn processar() !void {
const recurso = try adquirirRecurso();
errdefer |err| {
std.log.err("Falha ao processar: {}", .{err});
recurso.liberar();
};
try etapa1();
try etapa2();
try etapa3();
}
A variável err contém o erro que causou a saída do escopo, permitindo logging detalhado.
Quando Usar Defer vs Errdefer
fn exemplo(allocator: std.mem.Allocator) !*Dados {
const dados = try allocator.create(Dados);
// ERRADO: defer destruiria mesmo no retorno com sucesso!
// defer allocator.destroy(dados);
// CERTO: errdefer só destrói se houver erro
errdefer allocator.destroy(dados);
dados.* = try Dados.inicializar();
return dados; // Chamador agora é dono da memória
}
A regra é simples: se a função retorna o recurso ao chamador, use errdefer. Se a função consome o recurso internamente, use defer.
Armadilhas Comuns
- Usar
deferonde deveria sererrdefer: Se a função retorna um recurso alocado,defero destruiria antes do chamador usá-lo. - Esquecer o
errdeferem cadeias de alocação: Cada recurso alocado antes de um possíveltryprecisa de seu próprioerrdefer. - Ordem dos errdefers: Assim como
defer, oserrdefers executam em ordem LIFO. Certifique-se de que a ordem de limpeza faz sentido. - Não usar quando não há transferência de propriedade: Se o recurso será liberado na mesma função,
deferé suficiente.
Casos de Uso
O errdefer resolve um problema clássico: como garantir que recursos alocados em etapas sejam liberados corretamente quando uma das etapas falha?
Inicialização em múltiplos passos é o caso de uso principal. Considere um cliente de banco de dados que aloca conexão, prepara statements e cria um pool de workers:
const DbCliente = struct {
conn: *Conexao,
stmt_pool: []Statement,
workers: []Worker,
pub fn init(allocator: std.mem.Allocator) !DbCliente {
const conn = try Conexao.abrir("postgres://localhost/db");
errdefer conn.fechar();
const stmt_pool = try alocaStatements(allocator, conn, 10);
errdefer liberaStatements(allocator, stmt_pool);
const workers = try iniciaWorkers(allocator, 4);
// Sem errdefer aqui — se chegamos, tudo OK
return .{
.conn = conn,
.stmt_pool = stmt_pool,
.workers = workers,
};
}
pub fn deinit(self: *DbCliente, allocator: std.mem.Allocator) void {
liberaWorkers(allocator, self.workers);
liberaStatements(allocator, self.stmt_pool);
self.conn.fechar();
}
};
Cada errdefer protege exatamente o que foi alocado até aquele ponto. Se iniciaWorkers falhar, apenas os recursos dos passos anteriores são limpos — nem mais nem menos.
Boas Práticas
- Sempre pareie
errdefercom alocações antes detry: Cadatryé um ponto de saída potencial por erro; cada alocação antes de umtryprecisa de proteção. - Use
errdefer |err|para diagnóstico: Capturar o erro no errdefer permite emitir mensagens de log detalhadas que facilitam o debug. - Não use
errdeferquandodeferé suficiente: Se o recurso é consumido inteiramente dentro da função,deferé mais simples e correto.
Termos Relacionados
- Defer — Execução garantida ao sair do escopo
- Error Union — Tipo que carrega valor ou erro
- Try — Propagação automática de erros
- Allocator — Abstração de alocação de memória
- RAII em Zig — Padrão de gerenciamento de recursos