Para a maioria dos projetos Go novos em 2026, comece com log/slog. Ele faz parte da biblioteca padrão, oferece logging estruturado, níveis, grupos, atributos tipados e uma interface de Handler que permite trocar o destino sem contaminar toda a aplicação. Zap continua uma escolha forte quando o time já usa o ecossistema da Uber ou mediu uma hot path em que encoder e alocações importam. Zerolog é excelente quando a prioridade é JSON eficiente com uma API encadeada e baixo overhead.
A resposta curta, portanto, não é “o mais rápido ganha”. O melhor logger é o que entrega campos consistentes, redaction de dados sensíveis, correlação com traces, volume controlado e uma API que o time usa corretamente. Este comparativo mostra as diferenças práticas entre slog, Zap e Zerolog, com código, critérios de decisão, armadilhas e um plano de migração. Para aprender os fundamentos antes da comparação, leia o guia completo de slog e logging estruturado.
Resposta rápida: qual escolher?
| Situação | Recomendação |
|---|---|
| Projeto novo, API ou worker comum | slog |
| Biblioteca que não deve impor dependência de logging | slog ou nenhuma API de logger pública |
| Stack existente já padronizada em Zap | Zap |
| Hot path comprovada por benchmark/profile | Zap ou Zerolog, depois de medir no seu cenário |
| JSON compacto, API fluente e baixa alocação | Zerolog |
| Vários backends com uma API estável | slog + Handler/adaptador |
| Migração gradual de logger legado | slog na fronteira nova, com adaptador temporário |
Regra prática: adote slog por padrão e só aceite o custo operacional de uma dependência especializada quando houver uma razão concreta: compatibilidade existente, recurso indispensável ou medição reproduzível.
O que realmente muda entre slog, Zap e Zerolog?
As três opções produzem logs estruturados e suportam níveis. A diferença aparece na ergonomia, no modelo de extensão e no custo de acoplamento.
| Critério | slog | Zap | Zerolog |
|---|---|---|---|
| Origem | Biblioteca padrão (log/slog) | go.uber.org/zap | github.com/rs/zerolog |
| Estilo de API | Mensagem + slog.Attr | Campos tipados ou SugaredLogger | Encadeamento fluente |
| Extensão | Interface slog.Handler | zapcore.Core e encoders | Hooks, writers e adapters |
| Dependência externa | Não | Sim | Sim |
| JSON em produção | JSONHandler | NewProduction/encoder | Foco principal |
| Console legível | TextHandler | NewDevelopment | ConsoleWriter |
| Sampling | Implementado no Handler/ecossistema | Embutido em configurações de produção | Embutido |
API estilo printf | Não é o foco | Sim, via SugaredLogger | Não é o foco |
| Melhor argumento | Padrão estável e desacoplado | Ecossistema maduro e controle fino | API simples orientada a performance |
Nenhuma linha dessa tabela substitui um teste no seu ambiente. Um serviço que escreve 200 eventos por segundo para stdout tem necessidades diferentes de um proxy que tenta registrar dezenas de milhares de eventos por segundo. E um logger rápido não salva uma aplicação que serializa payloads gigantes ou registra a mesma falha em quatro camadas.
A mesma operação nos três loggers
Comparar a sintaxe ajuda a entender o custo cognitivo para o time.
slog
logger.InfoContext(ctx, "pedido processado",
slog.String("order_id", orderID),
slog.String("customer_id", customerID),
slog.Duration("duration", elapsed),
slog.Int("attempt", attempt),
)
A versão tipada com slog.String, slog.Int e slog.Duration deixa o contrato explícito. Também existe a forma curta com pares chave/valor, mas atributos tipados reduzem erros silenciosos em código crítico.
Zap
logger.Info("pedido processado",
zap.String("order_id", orderID),
zap.String("customer_id", customerID),
zap.Duration("duration", elapsed),
zap.Int("attempt", attempt),
)
O zap.Logger tipado é parecido com slog. O SugaredLogger oferece Infow e métodos no estilo printf, mais convenientes, mas com menos segurança e overhead adicional em relação à API tipada.
Zerolog
logger.Info().
Str("order_id", orderID).
Str("customer_id", customerID).
Dur("duration", elapsed).
Int("attempt", attempt).
Msg("pedido processado")
A API encadeada é compacta e agradável quando o time já conhece o padrão. A armadilha é esquecer Msg, Send ou a conclusão equivalente: o evento é construído, mas não é emitido.
Por que slog virou o default sensato
slog entrou na biblioteca padrão no Go 1.21. Isso não tornou Zap e Zerolog obsoletos; mudou o ponto de partida da decisão. Antes, todo projeto precisava escolher uma dependência e inventar uma abstração. Agora existe uma API comum, mantida junto com Go, que cobre o caso geral.
O componente mais importante é o slog.Handler. Ele recebe registros, decide se um nível está habilitado e transforma atributos em saída. O bootstrap pode usar slog.NewJSONHandler, enquanto testes usam um handler em memória e uma plataforma específica usa um handler compatível com seu backend.
func newLogger(env string) *slog.Logger {
var handler slog.Handler
if env == "production" {
handler = slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
})
} else {
handler = slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
Level: slog.LevelDebug,
})
}
return slog.New(handler).With(
slog.String("service", "checkout-api"),
slog.String("env", env),
)
}
Essa escolha combina bem com dependency injection sem framework: construtores recebem *slog.Logger, derivam um logger com component e mantêm a configuração no main. O domínio não precisa saber se os registros terminam em stdout, OpenTelemetry ou um coletor proprietário.
Quando Zap ainda é a melhor escolha
Zap é maduro, conhecido e amplamente integrado a bibliotecas de infraestrutura. Se uma base existente já usa zap.Logger, trocar apenas porque slog existe pode gerar trabalho sem ganho para usuário, confiabilidade ou custo.
Zap também oferece controle fino por meio de zapcore: encoder, nível, destino, combinação de cores, clocks e cores múltiplos. Isso é útil em plataformas que precisam, por exemplo, enviar erros para um sink e informações para outro.
cfg := zap.NewProductionConfig()
cfg.Level = zap.NewAtomicLevelAt(zap.InfoLevel)
cfg.EncoderConfig.TimeKey = "timestamp"
cfg.EncoderConfig.MessageKey = "message"
logger, err := cfg.Build()
if err != nil {
return err
}
defer func() { _ = logger.Sync() }()
Os pontos de atenção são o acoplamento dos campos zap.Field por toda a base e a escolha entre Logger e SugaredLogger. Se cada pacote expõe Zap em sua API pública, uma migração futura fica cara. Em aplicações, esse custo pode ser aceitável; em libraries reutilizáveis, normalmente não é.
Quando Zerolog faz sentido
Zerolog foi desenhado em torno de eventos JSON eficientes, sem reflection no caminho comum e com uma API fluente. Ele funciona muito bem em serviços com alto volume de logs, desde que esse volume seja realmente necessário e medido.
logger := zerolog.New(os.Stdout).
With().
Timestamp().
Str("service", "gateway").
Logger()
logger.Warn().
Str("upstream", "payments").
Int("status", 429).
Dur("retry_after", 2*time.Second).
Msg("upstream aplicou rate limit")
Zerolog oferece sampling, hooks, integração com context.Context, stack traces opcionais e ConsoleWriter para desenvolvimento. O console formatado, porém, é para ergonomia local; produção deve preservar um formato estruturado previsível.
A principal pergunta é se o estilo encadeado será usado de forma consistente. Uma API veloz com campos improvisados (user, userId, user_id) continua produzindo observabilidade ruim. Padronização de nomes vale mais que a diferença entre duas bibliotecas rápidas.
Performance: não escolha pelo benchmark do README
Zap e Zerolog publicam benchmarks e têm arquiteturas voltadas a reduzir alocações. Eles frequentemente superam configurações genéricas em testes sintéticos. Isso é informação útil, mas não é uma decisão completa por cinco motivos:
- O Handler muda o resultado. Comparar
slogexige dizer qual Handler está sendo usado. - O destino muda tudo. Escrever em buffer, arquivo, stdout capturado por container ou rede tem custos diferentes.
- A forma do evento importa. Dez strings pequenas não equivalem a um erro com stack e um objeto refletido.
- Nível desabilitado importa. O custo de uma chamada
Debugfiltrada pode ser mais relevante que o encode de um evento emitido. - Ingestão custa dinheiro. Um logger rápido pode facilitar logging excessivo e aumentar a conta da plataforma.
Faça um benchmark representativo com o pacote testing:
func BenchmarkSlogOrder(b *testing.B) {
logger := slog.New(slog.NewJSONHandler(io.Discard, nil))
ctx := context.Background()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
logger.InfoContext(ctx, "pedido processado",
slog.String("order_id", "ord_123"),
slog.Int("attempt", 2),
slog.Duration("duration", 18*time.Millisecond),
)
}
}
Repita com Zap e Zerolog, usando o mesmo evento, nível e writer. Rode go test -bench=. -benchmem e compare com a carga real. O guia de benchmarks em Go explica como evitar conclusões frágeis; se o logger aparece em CPU ou alocações de produção, confirme com pprof.
Context, request ID e OpenTelemetry
O logger não deve virar um depósito mágico dentro de context.Context. O padrão mais simples é injetar o logger como dependência e guardar no contexto apenas valores ligados à operação, como request_id, trace_id e deadline.
Na borda HTTP, derive um logger com os campos da requisição:
func requestLogger(base *slog.Logger, r *http.Request) *slog.Logger {
return base.With(
slog.String("request_id", r.Header.Get("X-Request-ID")),
slog.String("method", r.Method),
slog.String("path", r.URL.Path),
)
}
Se o serviço usa tracing, adicione trace_id e span_id de forma consistente. Isso permite sair de uma linha de log e chegar ao trace completo. Veja o guia de OpenTelemetry em Go e o tutorial de observabilidade para aplicações Go.
Os três loggers podem participar dessa arquitetura. O diferencial não é “suporta context”, mas se sua aplicação consegue correlacionar uma requisição sem vazar detalhes de infraestrutura para cada função de domínio.
Sampling, níveis e controle de volume
Em alto tráfego, registrar cada evento repetitivo é desperdício. Zap e Zerolog trazem recursos de sampling; em slog, o comportamento pode ser implementado por um Handler ou obtido no ecossistema.
Antes de ativar sampling, classifique os eventos:
- erros raros de integridade: preserve todos;
- 404 de bot ou health check: agregue ou filtre;
- retry transitório conhecido: use
Warncom sampling; - sucesso por request: considere métrica, não log individual;
- diagnóstico detalhado:
Debug, desligado por padrão.
Métricas respondem “quantos?”; logs explicam “o que aconteceu?”; traces mostram “onde o tempo foi gasto?”. Não use milhões de linhas para substituir um contador Prometheus. O tutorial de Prometheus em Go ajuda a separar esses sinais.
Redaction e segurança valem para os três
Nunca escolha um logger sem planejar como remover senha, token, cookie, chave de API, CPF, cartão e payloads pessoais. Centralize a política no Handler/Core/Hook, mas também evite passar o dado sensível desde a origem.
Para slog, ReplaceAttr resolve campos conhecidos:
func redact(_ []string, attr slog.Attr) slog.Attr {
switch strings.ToLower(attr.Key) {
case "authorization", "cookie", "password", "token", "api_key":
return slog.String(attr.Key, "[REDACTED]")
default:
return attr
}
}
Redaction por nome não protege um token escondido no campo payload. Defina uma allowlist de campos para bordas críticas, evite logar structs inteiras e cubra o comportamento com testes. O guia de segurança em Go aprofunda práticas para APIs e serviços.
Como migrar sem reescrever tudo
Uma migração segura acontece por fronteiras, não com busca e substituição global.
- Inventarie os campos atuais. Liste chaves, níveis, stack traces e formatos consumidos por alertas.
- Defina um contrato. Padronize
service,component,request_id,trace_id,duration_ms,errore nomes de negócio. - Escolha a API para código novo. Em geral,
*slog.Loggerou uma interface realmente pequena. - Adapte no bootstrap. Use um Handler para o backend necessário em vez de levar dois loggers a cada componente.
- Migre módulo por módulo. Comece em handlers HTTP, consumers e jobs; depois avance para infraestrutura.
- Compare a saída. Verifique JSON, níveis, timestamp, stack, sampling e redaction.
- Remova o legado só no fim. Evite uma camada universal permanente que esconda todos os recursos e agrade ninguém.
Não crie uma interface com 25 métodos apenas para “não depender” de Zap. Isso reproduz a biblioteca inteira, adiciona manutenção e dificulta atributos tipados. Se o domínio precisa reportar uma falha, muitas vezes retornar um erro rico para a borda é melhor do que receber um logger.
Armadilhas comuns na escolha
- Escolher pelo menor
ns/opsem profile real. Otimização sem gargalo só compra complexidade. - Logar o mesmo erro em repository, service e handler. Retorne com contexto e registre uma vez na borda.
- Misturar mensagem dinâmica e campo. Use mensagem estável (
"pedido falhou") eorder_idseparado. - Usar
Any/reflection para tudo. Campos tipados são mais previsíveis e evitam serializar objetos enormes. - Deixar bibliotecas decidirem formato global. Uma library deve retornar erros ou aceitar integração discreta, não reconfigurar o logger da aplicação.
- Ignorar flush/sync. Zap pode exigir
Sync; buffers e writers customizados também precisam de encerramento correto. Combine com graceful shutdown. - Colocar segredo no log e tentar corrigir só no coletor. A informação já saiu do processo; a proteção deve começar na aplicação.
- Confundir logging com auditoria. Log operacional pode sofrer sampling e retenção curta; trilha de auditoria exige modelo, integridade e retenção próprios.
Rubrica de decisão para o seu time
Dê de 0 a 2 pontos para cada opção em cada pergunta:
- Já existe padrão consolidado e dashboards dependentes dela?
- A hot path aparece em benchmark e profile de produção?
- O time precisa de sampling/encoders específicos sem escrever infraestrutura própria?
- Uma dependência adicional é aceitável para manutenção e supply chain?
- A API ficará restrita ao bootstrap ou vazará para centenas de pacotes?
- Existem testes do formato e da redaction?
Se não há necessidade especializada, slog vence pela simplicidade. Se Zap já domina a base e atende o contrato, permanecer em Zap costuma ser mais racional. Se benchmarks reais mostram que logging JSON é um gargalo relevante e o estilo fluente agrada ao time, Zerolog merece o teste.
Para projetos de portfólio e entrevistas, mostrar essa decisão é mais valioso do que instalar três bibliotecas. Implemente uma API com slog, request_id, JSON em produção, redaction e um teste; depois explique em quais condições você trocaria o Handler. Isso conversa diretamente com projetos Go para portfólio e com o tipo de observabilidade cobrado em vagas de Go no Brasil.
Perguntas frequentes
Qual é o melhor logger para Go em 2026?
Para a maioria dos projetos novos, comece com log/slog: ele está na biblioteca padrão, tem API estável e permite trocar o Handler sem acoplar o domínio a uma implementação. Escolha Zap quando o ecossistema existente ou uma hot path exige sua API e seu encoder. Escolha Zerolog quando JSON com baixa alocação e uma API encadeada são prioridades medidas, não apenas presumidas.
slog é mais lento que Zap e Zerolog?
Depende do Handler, dos atributos e do destino. Zap e Zerolog foram desenhados para reduzir alocações e costumam se destacar em benchmarks sintéticos. Em aplicações reais, I/O, serialização de objetos grandes e ingestão podem dominar. Compare o mesmo evento com -benchmem e confirme o impacto com profile antes de decidir.
Posso usar Zap ou Zerolog por trás da API slog?
Sim. Existem handlers e adaptadores no ecossistema para conectar slog a backends especializados. Isso permite expor *slog.Logger no código da aplicação e manter a implementação no bootstrap. Trate o adaptador como infraestrutura: verifique níveis, grupos, source location, stack, performance e formato JSON.
Como migrar de Zap ou Zerolog para slog?
Migre pelas bordas. Defina primeiro os nomes de campos e a saída esperada, use slog nos novos componentes e adapte o backend durante a transição. Converta módulos gradualmente e preserve alertas. Uma troca global sem testes pode quebrar queries, dashboards, sampling e redaction mesmo quando o código compila.
Logger deve ser colocado no context.Context?
Prefira injetar o logger e usar o contexto para dados e cancelamento da operação. Derive um logger na borda com request_id e trace_id. Se o projeto já carrega logger no contexto, encapsule o acesso em funções próprias e evite chaves soltas, seguindo os cuidados do guia de context e cancelamento.
Conclusão
slog é o melhor default; Zap e Zerolog são escolhas especializadas que continuam relevantes. O slog reduz dependências e oferece uma API padrão extensível. Zap entrega um ecossistema maduro, encoders e controle fino. Zerolog oferece uma API fluente focada em JSON eficiente e baixo overhead.
A decisão profissional considera mais que velocidade: contrato de campos, qualidade dos alertas, integração com traces, redaction, volume, custo de ingestão, familiaridade do time e acoplamento. Comece simples, meça com um evento representativo e só otimize quando o profile mostrar que logging merece atenção.
Próximos passos: