Errdefer em Zig — O que é e Como Usar

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

  1. Alocação parcial segura: Quando uma função aloca vários recursos em sequência, errdefer garante que os já alocados sejam liberados se um dos passos falhar.
  2. Evita vazamentos de memória: Sem errdefer, erros no meio de uma função podem deixar recursos sem liberação.
  3. Código mais legível: A lógica de limpeza fica junto da alocação, não em blocos de tratamento de erro distantes.
  4. Complementa o defer: Enquanto defer é 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 defer onde deveria ser errdefer: Se a função retorna um recurso alocado, defer o destruiria antes do chamador usá-lo.
  • Esquecer o errdefer em cadeias de alocação: Cada recurso alocado antes de um possível try precisa de seu próprio errdefer.
  • Ordem dos errdefers: Assim como defer, os errdefers 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 errdefer com alocações antes de try: Cada try é um ponto de saída potencial por erro; cada alocação antes de um try precisa 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 errdefer quando defer é 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

Tutoriais Relacionados

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.