slog em um exemplo
O log/slog (Go 1.21) escreve registros estruturados: uma mensagem, um nível e atributos chave-valor.
Cada linha sai como time=... level=INFO msg="user logged in" user=ada attempts=1. Como cada valor é um campo separado, um coletor de logs (Loki, Elasticsearch, CloudWatch, Datadog) consegue filtrar por user=ada ou level=ERROR sem expressões regulares sobre texto livre.
Os exemplos desta página escrevem em os.Stdout para que a saída apareça em ordem. Em um serviço real, os logs normalmente vão para os.Stderr, que também é onde o logger padrão escreve.
Níveis
| Nível | Valor | Use para |
|---|---|---|
slog.LevelDebug | -4 | detalhes para desenvolvedores, desligado em produção |
slog.LevelInfo | 0 | eventos normais: inicializou, requisição atendida, job concluído |
slog.LevelWarn | 4 | algo inesperado que o programa tratou |
slog.LevelError | 8 | uma operação falhou |
O handler descarta registros abaixo do seu nível mínimo, e o mínimo padrão é Info. É por isso que slog.Debug(...) não imprime nada até você configurar um handler com Level: slog.LevelDebug. Os intervalos entre os valores deixam espaço para níveis personalizados como slog.Level(2).
Para mudar o nível em tempo de execução (a partir de uma flag, de um endpoint de administração ou de um sinal), coloque um slog.LevelVar nas opções e chame Set nele depois:
var level slog.LevelVar // zero value: Info
logger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: &level}))
level.Set(slog.LevelDebug) // from now on, debug records are written
Texto ou JSON
slog.NewTextHandler escreve pares key=value, fáceis de ler em um terminal. slog.NewJSONHandler escreve um objeto JSON por linha, o formato que a maioria dos pipelines de logs espera. As chamadas de log continuam as mesmas; só o handler muda.
Os valores mantêm os seus tipos: status é um número na saída JSON e retry um booleano, um time.Duration aparece como 42ms no texto e como nanossegundos no JSON, e um error imprime a sua mensagem. O ReplaceAttr é o gancho para reescrever ou remover atributos, usado aqui para descartar o horário e, na prática, para renomear chaves (msg para message) ou mascarar valores.
Atributos
A forma com tipagem solta alterna chaves e valores: "user", "ada", "attempts", 3. Ela é curta e tem um único modo de falha: um número ímpar de argumentos. O valor que sobra é registrado sob a chave !BADKEY. O go vet pega isso:
./main.go:14:2: call to slog.Info missing a final value
Para segurança de tipos e um pouco menos de alocação, use os construtores de atributos, e LogAttrs ao registrar logs em um caminho quente:
logger.Info("order placed",
slog.Int("order_id", 1017),
slog.String("currency", "EUR"),
slog.Float64("total", 59.90),
slog.Duration("took", elapsed),
)
logger.LogAttrs(ctx, slog.LevelInfo, "order placed", slog.Int("order_id", 1017))
Use uma única convenção de nomes para as chaves em toda a base de código (user_id em todo lugar, e não userID em um pacote e uid em outro). As consultas no seu sistema de logs dependem disso.
With: loggers que carregam contexto
logger.With(attrs...) devolve um novo logger que acrescenta esses atributos a todo registro. Crie um por requisição ou por job, e todas as linhas que ele escrever podem ser ligadas entre si:
Toda linha carrega service, version, request_id e user sem repeti-los em cada chamada. slog.Group aninha atributos, que o handler JSON escreve como um objeto aninhado ("payment":{"amount":25,"currency":"USD"}) e o handler de texto como chaves com ponto (payment.amount=25). logger.WithGroup("db") coloca todos os atributos seguintes daquele logger dentro de um grupo.
Passe o logger da requisição adiante como parâmetro ou campo de struct. Guardá-lo em um context.Context é possível, mas esconde a dependência; os métodos InfoContext(ctx, ...) do slog passam o context para o handler, que um handler personalizado pode usar para extrair trace IDs.
Escondendo segredos com LogValuer
Um tipo pode controlar como é registrado implementando slog.LogValuer. Isso mantém senhas e tokens fora dos logs, não importa quem registre o valor:
User registra só o ID e o email, e um Token registrado sozinho imprime REDACTED. O handler só chama LogValue quando o registro é de fato escrito, então isso também funciona para valores caros de calcular.
O pacote clássico log
O log é anterior ao slog e ainda serve bem para programas pequenos e scripts. Ele escreve linhas no erro padrão com um prefixo de data e hora:
| Flag | Acrescenta |
|---|---|
log.LstdFlags (o padrão) | data e hora 2009/11/10 23:00:00 |
log.Lmicroseconds | microssegundos no horário |
log.LUTC | horário em UTC |
log.Lshortfile / log.Llongfile | main.go:14 / o caminho completo |
log.Lmsgprefix | coloca o prefixo antes da mensagem em vez de no início da linha |
Três funções saem do programa ou causam panic, e a diferença importa:
log.Fatal,log.Fatalfelog.Fatallnimprimem e depois chamamos.Exit(1). As chamadas adiadas não executam. Use-as nomainpara falhas na inicialização, nunca em código de biblioteca ou em handlers de requisição.log.Panice as suas variantes imprimem e depois causam panic, então as chamadas adiadas executam e o panic pode ser recuperado.- Todas as outras funções apenas escrevem uma linha.
Para registrar em um arquivo, abra-o e passe-o para log.New ou log.SetOutput; io.MultiWriter(os.Stderr, f) escreve nos dois.
log e slog juntos
slog.SetDefault(logger) torna logger o padrão das funções de nível superior slog.Info e também faz a saída do pacote log passar por ele. As chamadas log.Printf existentes no seu código ou nas dependências passam a sair como registros estruturados de nível Info:
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stderr, nil)))
log.Printf("legacy message") // {"time":"...","level":"INFO","msg":"legacy message"}
Antes do SetDefault, o logger padrão do slog escreve por meio do pacote log, e é por isso que um slog.Info("hi") sozinho imprime 2026/09/23 14:30:00 INFO hi.
Regras práticas
- Registre ou devolva um erro, não os dois. Uma função que registra um erro e o devolve faz a mesma falha ser registrada em todos os níveis da pilha de chamadas. Devolva os erros para cima com contexto e registre uma vez, onde eles são tratados.
- Coloque os dados variáveis nos atributos, não na mensagem.
logger.Info("user created", "user_id", id)agrupa bem em um sistema de logs;logger.Info(fmt.Sprintf("user %d created", id))cria uma mensagem diferente para cada usuário. - Nunca registre segredos nem corpos completos de requisição. Use
LogValuerouReplaceAttrpara mascarar. - Use JSON em produção e texto em desenvolvimento. Escolha o handler na inicialização a partir de uma flag ou de uma variável de ambiente.
- Escolha os níveis com cuidado. Se tudo é registrado como Error, os alertas de erro viram ruído.
Perguntas frequentes
O que é o slog em Go?
O log/slog é o pacote de logs estruturados acrescentado à biblioteca padrão no Go 1.21. Em vez de strings formatadas, cada registro tem uma mensagem, um nível (Debug, Info, Warn, Error) e atributos chave-valor, e um handler o escreve como texto key=value ou como JSON: slog.Info("login", "user", "ada", "attempts", 3).
Como ativar os logs de debug no slog?
O nível mínimo padrão é Info, então slog.Debug não imprime nada. Crie um handler com um nível mais baixo e torne-o o padrão: slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug}))). Use um slog.LevelVar em vez de uma constante se quiser mudar o nível com o programa rodando.
Qual a diferença entre log e slog em Go?
O log escreve linhas livres com um prefixo opcional de data e hora e não tem níveis. O slog escreve registros com níveis e atributos chave-valor tipados que os coletores de logs conseguem interpretar e filtrar. Os dois estão na biblioteca padrão; slog.SetDefault também redireciona a saída do pacote log pelo handler do slog.
O log.Fatal executa as funções adiadas?
Não. log.Fatal e log.Fatalf imprimem a mensagem e chamam os.Exit(1), que pula todas as chamadas adiadas. Use-os só no main ou em código de inicialização em que não há nada para limpar. log.Panic causa panic no lugar, então as chamadas adiadas executam.