Code review em Zig não é só procurar estilo. A linguagem dá controle explícito sobre memória, erros, layout, compilação, integração com C e artefatos finais. Esse controle é ótimo para software de sistemas, CLIs, servidores e bibliotecas de performance, mas também muda o tipo de bug que passa por uma revisão superficial.
Um review bom em Zig responde perguntas concretas: quem é dono dessa memória? O erro libera tudo que já foi alocado? O allocator correto chega até a camada certa? O comptime facilita ou esconde complexidade? O binário foi testado no target que será distribuído? A dependência nova alterou a supply chain?
Este checklist é uma base prática para revisar pull requests em projetos Zig. Use como roteiro, não como burocracia. Para contexto complementar, veja também Clean Code em Zig, Error Handling em Zig, Alocação de Memória, Testes em Zig e Zig Supply Chain.
1. Comece pelo contrato da mudança
Antes de entrar linha por linha, entenda o que a mudança promete:
- adiciona API pública, comando de CLI, endpoint ou biblioteca interna?
- muda comportamento compatível ou quebra contrato?
- toca hot path, parser, rede, arquivo, criptografia ou memória compartilhada?
- roda em produção, build script, teste ou ferramenta local?
- afeta targets específicos, como Linux, macOS, Windows, ARM ou WebAssembly?
Em Zig, contexto importa muito. Uma alocação aceitável em comando administrativo pode ser ruim em loop de rede. Um panic tolerável em build tool pode ser inaceitável em biblioteca. Um @ptrCast em camada de interoperabilidade C pode ser necessário; espalhado pelo domínio da aplicação, provavelmente é sinal de design frágil.
2. Gerenciamento de memória
A primeira pergunta de review é propriedade: quem aloca, quem libera e por quanto tempo o dado vive?
const dados = try allocator.alloc(u8, 1024);
defer allocator.free(dados);
const copia = try allocator.dupe(u8, entrada);
errdefer allocator.free(copia);
try registrar(copia);
Verifique:
- todo
alloc,create,dupeouArrayList.inittemfree,destroyoudeinitcorrespondente; - caminhos de erro usam
errdeferquando a função ainda não transferiu propriedade; - structs com
inittêmdeinitclaro e documentado; - funções que retornam memória alocada dizem qual allocator deve liberar;
- slices retornados não apontam para stack frame encerrado;
ArenaAllocatornão está escondendo vazamento em processo longo;GeneralPurposeAllocatoroustd.testing.allocatoré usado em testes para detectar leaks.
Um padrão saudável é nomear propriedade no contrato:
/// Retorna uma string alocada com `allocator`. O chamador deve liberar.
pub fn renderMensagem(allocator: std.mem.Allocator, nome: []const u8) ![]u8 {
return try std.fmt.allocPrint(allocator, "Olá, {s}", .{nome});
}
Se o review precisa adivinhar quem libera, o código precisa de ajuste.
3. defer, errdefer e ordem de limpeza
defer e errdefer são fortes, mas ordem importa. Eles executam em ordem inversa de declaração. Em funções com vários recursos, confira se a limpeza acontece no sentido correto.
const file = try std.fs.cwd().openFile(path, .{});
defer file.close();
var buffer = try allocator.alloc(u8, size);
errdefer allocator.free(buffer);
try file.readAll(buffer);
return buffer; // propriedade transferida; errdefer não roda no sucesso
Perguntas de review:
deferlibera algo que ainda será usado depois?errdeferdeveria virardeferporque não há transferência de propriedade?- há múltiplos recursos que precisam ser liberados em ordem específica?
- uma função retorna antes de registrar o
defernecessário? - um objeto parcialmente inicializado tem
errdeferpara cada campo já adquirido?
Para inicialização de structs complexas, prefira etapas pequenas e limpeza explícita. O bug clássico é alocar o segundo campo, falhar no terceiro e vazar o primeiro.
4. Tratamento de erros sem engolir falhas
Zig força error unions, mas ainda é possível destruir observabilidade com catch {} ou transformar tudo em erro genérico.
// Fraco: o erro desaparece.
_ = gravarArquivo() catch {};
// Melhor: contexto, propagação ou fallback explícito.
gravarArquivo() catch |err| {
std.log.err("falha ao gravar relatório {s}: {}", .{ caminho, err });
return err;
};
Revise:
tryestá no nível certo ou deveria adicionar contexto antes de propagar?catch unreachableé realmente impossível ou só conveniente?- erros de input externo viram resposta controlada, não
panic; - bibliotecas não chamam
std.process.exitoupanicpara erro recuperável; - logs têm contexto suficiente sem expor segredo;
- error sets públicos são estáveis e compreensíveis.
Em APIs públicas, cuidado com error set enorme que vaza detalhe interno. Em aplicações, cuidado com erro genérico que não ajuda a operar produção.
5. Allocator correto para cada camada
Allocator é decisão arquitetural. Em review, confira se o allocator vem de fora quando a função é reutilizável:
pub fn parseConfig(allocator: std.mem.Allocator, bytes: []const u8) !Config {
// bom: chamador escolhe arena, GPA, testing allocator etc.
}
Evite criar allocator global escondido em biblioteca. Em servidor, prefira separar:
- allocator de processo para estruturas de longa vida;
- arena por request ou por job quando tudo morre junto;
- buffers reutilizáveis em hot path;
std.testing.allocatorem testes.
Se a mudança troca ArenaAllocator por alocações individuais, peça justificativa. Se troca alocações individuais por arena, confira se nenhum ponteiro da arena escapa além do ciclo de vida.
6. Slices, ponteiros e aliasing
Muitos bugs de Zig aparecem como slice válido com vida errada. Revise:
- slice retornado aponta para buffer de chamador ou memória própria?
- função modifica slice que parece somente leitura?
[]u8deveria ser[]const u8?- ponteiros opcionais (
?*T) são checados antes de uso? @ptrCast,@alignCaste@constCastestão isolados e justificados?- há suposição de alinhamento ou endianess documentada?
Prefira reduzir área insegura:
const bytes: []const u8 = std.mem.asBytes(&header);
Quando @ptrCast for necessário, deixe o código próximo da validação de tamanho/alinhamento e cubra com teste.
7. comptime e generics legíveis
comptime é uma das melhores partes de Zig, mas review deve separar abstração útil de metaprogramação decorativa.
Pergunte:
- o
comptimereduz duplicação real ou só torna erro mais difícil? - mensagens de erro para uso incorreto são claras?
- há
@compileErroramigável quando o tipo não atende ao contrato? - código gerado mantém tamanho de binário aceitável?
- testes cobrem pelo menos dois tipos/instâncias relevantes?
Exemplo de contrato melhor:
fn Repository(comptime T: type) type {
if (!@hasDecl(T, "id")) {
@compileError("Repository(T) exige campo ou decl id");
}
return struct { /* ... */ };
}
Se o código usa @typeInfo, revise com calma. Bugs de comptime costumam ser raros, mas quando quebram, a mensagem pode atingir todo consumidor da biblioteca.
8. Interoperabilidade com C
Zig chama C com facilidade. O review precisa tratar essa fronteira como área de risco.
Confira:
- headers importados por
@cImportsão estáveis e versionados; - tipos C têm conversão explícita para tipos Zig;
- strings C terminadas em zero são validadas;
- ownership de ponteiros C está documentado;
- funções C que retornam código de erro são checadas;
- biblioteca linkada é a esperada no target final;
- build.zig não depende de caminho local do autor.
Se um ponteiro vem de C, quem libera? Se Zig passa callback para C, qual é a vida do contexto? Essas duas perguntas pegam muitos problemas.
9. Performance e hot paths
Zig atrai projetos de performance, mas review não deve aceitar micro-otimização sem evidência. Procure primeiro problemas óbvios:
- alocação dentro de loop apertado;
- cópia desnecessária de buffers grandes;
- formatação de string em caminho quente;
- lookup linear onde mapa ou índice simples resolveria;
- locks ou atomics sem necessidade;
- branch complexa em parser crítico;
ReleaseFastescondendo comportamento que deveria ser seguro.
Exemplo típico:
var buffer = std.ArrayList(u8).init(allocator);
defer buffer.deinit();
for (items) |item| {
buffer.clearRetainingCapacity();
try buffer.appendSlice(item);
try processar(buffer.items);
}
Se a mudança afirma ganho de performance, peça benchmark reproduzível. O guia de benchmarking em Zig e o material de depuração e profiling ajudam a validar sem chute.
10. Segurança de input, logs e segredos
Revise todo ponto onde dado externo entra: argumentos de CLI, arquivo, rede, variável de ambiente, JSON, header HTTP, path, payload binário ou resposta de processo.
Checklist:
- tamanhos são limitados antes de alocar;
- parser diferencia erro de formato, EOF e limite excedido;
- paths não permitem traversal acidental;
- logs não imprimem token, senha, cookie, chave ou DSN;
- mensagens de erro externas não vazam detalhe sensível;
- dados binários não são tratados como UTF-8 sem validação;
- valores de ambiente obrigatórios são validados no boot;
- defaults inseguros não entram em produção.
Para configuração, conecte o review ao guia de segredos e variáveis de ambiente em Zig. Para dependências e releases, conecte ao guia de supply chain.
11. Testes que realmente protegem
Procure teste no mesmo nível do risco:
- unidade para função pura;
- teste com
std.testing.allocatorpara memória; - fixture para parser;
- integração para arquivo, rede ou CLI;
- regressão para bug corrigido;
- teste por target quando a mudança é multiplataforma.
Um teste útil em Zig costuma verificar também erro e limpeza:
test "parse rejeita entrada grande" {
const allocator = std.testing.allocator;
const entrada = try allocator.alloc(u8, 1024 * 1024);
defer allocator.free(entrada);
try std.testing.expectError(error.InputTooLarge, parse(allocator, entrada));
}
Se a mudança mexe em memória, rode testes com allocator de teste. Se mexe em compilação cruzada, rode pelo menos build do target relevante. Se mexe em CLI, capture saída e exit code.
12. Build, formatação e CI
Antes de aprovar, procure evidência de comandos básicos:
zig fmt --check src build.zig
zig build test
zig build -Doptimize=ReleaseSafe
Dependendo do projeto, acrescente:
zig build -Dtarget=x86_64-linux-musl
zig build -Dtarget=aarch64-macos
zig build docs
Revise também o build.zig:
- opções têm nomes claros;
- targets e optimize vêm de
standardTargetOptionsestandardOptimizeOptionquando apropriado; - testes estão ligados ao step padrão esperado;
- paths não são absolutos da máquina do autor;
- flags inseguras são comentadas;
- dependências novas aparecem no
build.zig.zoncom hash.
13. Documentação e exemplos
Mudança de API sem exemplo vira dívida. Verifique:
- README ou docs mostram o caminho feliz;
- exemplo compila com a versão atual;
- nomes e comentários batem com comportamento real;
- limitações são explícitas;
- breaking change aparece em changelog;
- tutorial não recomenda
catch unreachableou allocator global sem explicar contexto.
Em Zig, exemplos quebrados doem mais porque a linguagem e o tooling ainda evoluem. Um snippet antigo pode confundir iniciante rapidamente.
14. Supply chain da própria mudança
Para PRs que alteram dependências, CI, release ou Docker, faça review operacional:
- versão do Zig está fixada;
build.zig.zonusa URL e hash coerentes;- actions não usam
latestsem motivo; - imagem Docker tem tag ou digest;
- artefatos de release têm checksum;
- token de publicação só existe no job de publicação;
- SBOM ou inventário é atualizado quando exigido;
- changelog cita dependência ou toolchain nova.
Esse bloco conversa diretamente com GitHub Actions para releases Zig e Zig Supply Chain. Code review não termina no .zig; o build também é código.
15. Checklist rápido para colar no PR
Use este resumo quando precisar revisar rápido:
- propriedade de memória clara;
-
defer/errdefercobre sucesso e erro; - sem slice apontando para vida encerrada;
- error handling preserva contexto;
- allocator vem do chamador quando a função é reutilizável;
-
@ptrCast,@alignCast,@constCastsão necessários e isolados; - input externo tem limite e validação;
- logs não expõem segredo;
- testes cobrem caminho feliz, erro e regressão;
-
zig fmtezig build testrodaram; - build.zig/build.zig.zon não introduzem caminho local ou dependência flutuante;
- documentação e exemplos foram atualizados;
- mudança de performance tem benchmark ou justificativa mensurável;
- release/CI continua reproduzível.
Comparação com Rust e Go
Em Rust, o borrow checker captura parte dos problemas de memória antes do review. Em Go, garbage collector e convenções simples reduzem a superfície de ownership manual. Zig fica em outro ponto: dá controle explícito sem esconder custo. Por isso, o review humano precisa olhar propriedade, lifetime, erro e build com mais disciplina.
Isso não torna Zig “mais perigoso” por definição. Torna a revisão mais parecida com engenharia de sistemas: menos fé no framework, mais clareza no contrato. Para comparar práticas, veja comunidades de Rust no Brasil e Go no Brasil.
Conclusão
Code review em Zig é uma ferramenta de design. Ele confirma se a mudança tem ownership claro, erros previsíveis, testes úteis, build reproduzível e fronteiras seguras com C, sistema operacional e dependências.
Não tente revisar tudo com o mesmo peso. Foque primeiro no risco: memória, input externo, concorrência, build, release e API pública. Depois ajuste estilo. Um PR bonito que vaza memória, engole erro ou publica binário irreproduzível ainda não está pronto.
Conteúdo relacionado
- Clean Code em Zig — princípios de clareza e manutenção.
- Error Handling em Zig — tratamento de erros sem esconder falhas.
- Alocação de Memória — estratégias de allocator e ownership.
- Testes em Zig — testes unitários, integração e regressão.
- Depuração e Profiling em Zig — validar bugs e performance.
- Zig Supply Chain — revisar dependências, hashes e releases.