Para colocar análise estática em um projeto Go sem transformar o CI em uma coleção de alertas ignorados, a combinação mais equilibrada é: go vet como base oficial, Staticcheck para encontrar bugs e usos suspeitos, e golangci-lint para centralizar execução e política. Você não precisa habilitar dezenas de regras no primeiro dia. O objetivo é impedir defeitos reais com pouco ruído, não vencer uma competição de quantidade de linters.
A recomendação direta é começar com go test ./..., go vet ./... e um conjunto pequeno no golangci-lint. Habilite Staticcheck dentro do agregador, fixe a versão usada no CI e exija justificativa para cada //nolint. Em código legado, bloqueie novos problemas antes de tentar pagar todo o passivo antigo de uma vez.
Este guia explica as diferenças entre as ferramentas, mostra uma configuração v2 do golangci-lint, integra o lint ao CI e apresenta uma estratégia de adoção gradual. Combine-o com o guia de testes em Go, a cheatsheet de testes e debug e o artigo sobre table-driven tests para formar uma esteira de qualidade completa.
gofmt, go vet, Staticcheck e golangci-lint não são a mesma coisa
Essas ferramentas atuam em camadas diferentes:
| Ferramenta | Papel principal | Vem com Go? | Melhor uso |
|---|---|---|---|
gofmt | Formatação determinística | Sim | Remover discussões de estilo visual |
go test | Executar testes e compilar pacotes | Sim | Validar comportamento e integração |
go vet | Encontrar construções suspeitas | Sim | Base oficial de análise estática |
| Staticcheck | Detectar bugs, APIs mal usadas e código problemático | Não | Análise técnica mais profunda |
| golangci-lint | Orquestrar vários linters | Não | Política única para desenvolvimento e CI |
gofmt não procura bugs. go test pode deixar passar um caminho não coberto. go vet é deliberadamente conservador. Staticcheck amplia a análise. Já o golangci-lint não é um linter único: ele executa analisadores selecionados, normaliza a saída e oferece configuração para exclusões, timeout, formatos e código novo.
Essa distinção evita um erro comum: rodar Staticcheck separadamente e também habilitá-lo no golangci-lint sem necessidade. O resultado costuma ser diagnóstico duplicado e CI mais demorado. Escolha um ponto principal de execução; em times, normalmente é o agregador.
O que o Staticcheck encontra na prática
Staticcheck agrupa verificações por famílias. As regras SA procuram problemas de correção; S sugere simplificações; ST trata de estilo; QF oferece quick fixes. Nem toda sugestão tem a mesma gravidade, então leia o código da regra exibido no diagnóstico antes de alterar uma função.
Instale a ferramenta para uso local:
go install honnef.co/go/tools/cmd/staticcheck@latest
Depois execute no módulo inteiro:
staticcheck ./...
Um exemplo direto é uma expressão regular constante e inválida:
func hasTicketID(value string) bool {
matched, _ := regexp.MatchString(`[A-Z+`, value)
return matched
}
O padrão não fecha a classe iniciada por [. Staticcheck reporta esse tipo de problema antes da execução. Uma suíte que nunca chama a função pode passar; análise estática consegue inspecionar a constante mesmo sem exercitar aquele caminho.
Outro grupo frequente envolve recursos e concorrência: defer colocado no escopo errado, contexto ignorado, ticker que não é interrompido, resultado de API descartado ou sincronização usada de maneira impossível. O analisador não substitui o race detector, porque a corrida depende do comportamento em execução, mas os dois se complementam:
go test -race ./...
staticcheck ./...
A regra prática é priorizar diagnósticos de correção e segurança antes de sugestões cosméticas. Uma simplificação pode esperar; um erro descartado ou uma condição impossível merece investigação imediata.
Quando usar golangci-lint
Use golangci-lint quando o projeto precisa de uma política reproduzível entre notebooks e CI. Ele ajuda a responder perguntas operacionais:
- quais analisadores são obrigatórios;
- quais diretórios ou arquivos são excluídos;
- quanto tempo a etapa pode consumir;
- como a saída aparece no pull request;
- quais exceções são permitidas;
- se o repositório bloqueia apenas código novo ou todo o histórico.
O agregador também reduz a necessidade de instalar e versionar cada ferramenta individualmente. Entretanto, ele não elimina a responsabilidade de entender cada regra. Habilitar um linter desconhecido e aceitar correções automáticas sem revisão é tão perigoso quanto ignorar todos os alertas.
Para instalar localmente, você pode usar o método oficial adequado ao seu sistema ou instalar o comando em um diretório de ferramentas. Em qualquer abordagem, confira a versão:
golangci-lint version
golangci-lint run ./...
No CI, fixe uma versão, em vez de depender de latest. Atualizações podem adicionar regras, alterar defaults ou migrar o formato da configuração. Uma mudança de linter deve entrar em um pull request próprio, com diff revisável.
Configuração v2 mínima e útil
O formato atual da configuração usa version: "2". Salve o arquivo como .golangci.yml na raiz:
version: "2"
run:
timeout: 5m
linters:
default: none
enable:
- errcheck
- govet
- ineffassign
- staticcheck
- unused
formatters:
enable:
- gofmt
- goimports
issues:
max-issues-per-linter: 0
max-same-issues: 0
O conjunto é intencionalmente pequeno:
- errcheck encontra retornos de erro ignorados;
- govet preserva a análise oficial;
- ineffassign detecta atribuições sem efeito;
- staticcheck cobre várias classes de defeito;
- unused aponta código não utilizado;
- gofmt/goimports mantêm formatação e imports previsíveis.
default: none torna a política explícita. Assim, uma atualização do agregador não ativa silenciosamente um novo conjunto padrão no seu repositório. Depois de algumas semanas com saída limpa, avalie regras como bodyclose, errorlint, misspell ou gosec conforme o tipo de sistema. Não adicione uma regra só porque ela existe.
Para descobrir quais analisadores estão disponíveis na versão instalada:
golangci-lint linters
golangci-lint run --help
A própria CLI é a fonte mais confiável para a versão que o seu CI realmente executa.
Erros ignorados: corrija com contexto, não mecanicamente
errcheck costuma gerar o primeiro grande lote de problemas. Alguns são óbvios:
f, err := os.Open(name)
if err != nil {
return err
}
defer f.Close()
O Close pode falhar, especialmente em arquivos abertos para escrita. Uma função que cria um arquivo deve preservar esse erro:
func writeReport(name string, data []byte) (err error) {
f, err := os.Create(name)
if err != nil {
return err
}
defer func() {
if closeErr := f.Close(); err == nil {
err = closeErr
}
}()
_, err = f.Write(data)
return err
}
Mas nem todo retorno ignorado exige o mesmo tratamento. Em resposta HTTP, uma falha de Write geralmente indica que o cliente desconectou. Talvez você queira registrar uma métrica; talvez não exista ação útil naquele ponto. A análise estática abre a conversa — ela não conhece o contrato de negócio.
Quando ignorar for uma decisão consciente, deixe a intenção visível:
// A conexão já está sendo encerrada; não há recuperação útil neste ponto.
_ = conn.Close()
Ou, quando a ferramenta exigir uma supressão:
_ = writer.Flush() //nolint:errcheck // resposta já foi enviada; erro não é recuperável
Evite //nolint sem nome. Uma exceção ampla pode esconder no futuro um diagnóstico diferente na mesma linha.
Exclusões: específicas, locais e com prazo
Exclusões globais são tentadoras em repositórios antigos, mas se tornam caixas-pretas. Antes de ignorar uma regra no projeto inteiro, pergunte:
- O diagnóstico indica bug real?
- A regra conflita com uma convenção documentada do time?
- O problema está em código gerado?
- A exceção pode ficar ao lado do código?
- Existe issue para remover a exclusão?
Código gerado é o caso mais legítimo. Arquivos produzidos por mockgen, protoc, sqlc, templ ou outras ferramentas devem ser corrigidos na origem, não editados manualmente. Configure a exclusão conforme a sintaxe suportada pela versão instalada e confirme com:
golangci-lint run --verbose ./...
Para mocks próprios, revise também o guia de mocks com Testify, GoMock, fakes e httptest. Um mock gerado com versão antiga pode introduzir ruído que desaparece ao atualizar o gerador.
Adoção em projeto legado sem pull request infinito
Ativar cinco linters em um monólito antigo pode produzir milhares de linhas. Não faça uma correção massiva misturada com feature. Use uma sequência controlada:
- Fixe a versão da ferramenta.
- Execute localmente e exporte a lista de problemas.
- Classifique por risco: segurança, correção, recursos, manutenção e estilo.
- Corrija os itens críticos em commits pequenos.
- Bloqueie novas violações em relação à branch principal.
- Reduza o baseline por pacote nas semanas seguintes.
O golangci-lint permite analisar problemas novos a partir de uma revisão Git. Em CI, uma forma comum é comparar com a branch principal:
golangci-lint run --new-from-rev=origin/main ./...
Esse comando é especialmente útil no começo: o legado continua visível para uma iniciativa de limpeza, mas cada pull request precisa sair sem aumentar a dívida. Garanta que o checkout do CI tenha histórico suficiente para encontrar origin/main; clones rasos demais podem quebrar a comparação.
Não deixe o modo de baseline virar permanente. Registre uma meta, por exemplo: limpar um pacote por sprint ou zerar uma família de regras antes de habilitar outra.
Pipeline de CI recomendado
Uma esteira Go pequena pode separar compilação/testes de análise estática:
go test ./...
go test -race ./...
go vet ./...
golangci-lint run ./...
Em projetos grandes, o race detector pode ficar em um job separado ou rodar em um subconjunto frequente, porque custa mais CPU e tempo. O lint deve permanecer rápido o bastante para dar feedback cedo.
No GitHub Actions, use a action oficial do golangci-lint com uma versão fixada da action e da ferramenta. A estrutura conceitual é:
name: quality
on:
pull_request:
push:
branches: [main]
jobs:
test-and-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true
- run: go test ./...
- run: go vet ./...
- uses: golangci/golangci-lint-action@v9
with:
version: v2.12.2
args: --timeout=5m
As versões acima estavam atuais na publicação deste guia. Mesmo assim, escolha uma versão validada pelo seu projeto e atualize deliberadamente. Se você copiar este arquivo meses depois, consulte a documentação oficial antes de assumir que elas continuam adequadas.
Para supply chain, aplique o mesmo cuidado usado em GoReleaser, checksums e SBOM: action e binário de qualidade também fazem parte da sua cadeia de build. Atualização automática sem revisão pode mudar o resultado da entrega.
Lint não substitui testes, revisão nem govulncheck
Análise estática enxerga padrões no código; ela não sabe se o desconto calculado está certo, se uma permissão deveria ser negada ou se a API mantém compatibilidade. Continue escrevendo testes de unidade, integração e contrato.
Também não confunda linter com scanner de vulnerabilidades conhecidas. Execute govulncheck para relacionar dependências vulneráveis com caminhos chamados pelo programa, conforme o guia de vulnerabilidades em dependências Go:
govulncheck ./...
Uma política equilibrada cobre quatro perguntas diferentes:
- O código compila e se comporta como esperado?
go test. - Existe corrida observável?
go test -race. - Há construções suspeitas ou dívida detectável?
go vet, Staticcheck e outros linters. - Chamamos dependências com vulnerabilidades conhecidas?
govulncheck.
Na revisão humana, ainda é preciso avaliar clareza, modelagem, limites de pacotes, observabilidade, segurança e impacto operacional. Para uso responsável de assistentes, veja também IA no desenvolvimento Go em produção: código gerado por IA deve passar pela mesma esteira, nunca receber exceção automática.
Checklist de uma política de lint saudável
Antes de tornar o job obrigatório, confirme:
- A versão do golangci-lint está fixada no CI.
- A configuração declara
version: "2". - O conjunto inicial é pequeno e conhecido pelo time.
-
go testego vetcontinuam na esteira. - Staticcheck não roda duplicado sem motivo.
- Código gerado é tratado na origem ou excluído de forma específica.
- Cada
//nolintinforma a regra e o motivo. - O timeout é explícito e compatível com o tamanho do repositório.
- Projetos legados bloqueiam novos problemas e têm plano para reduzir o baseline.
- Atualizações de ferramentas entram em pull requests separados.
Uma boa política de lint quase desaparece no cotidiano: roda rápido, encontra problemas acionáveis e raramente exige discussão. Se o time está ignorando centenas de alertas, o problema não é falta de disciplina individual; a configuração precisa ser reduzida e recalibrada.
Perguntas frequentes
Qual é a diferença entre go vet, Staticcheck e golangci-lint?
go vet é a análise oficial distribuída com Go. Staticcheck adiciona verificações de bugs, performance, simplificação e uso incorreto de APIs. golangci-lint orquestra vários analisadores — inclusive govet e Staticcheck — e centraliza configuração, exclusões e integração com CI.
Preciso executar Staticcheck separadamente do golangci-lint?
Não, se Staticcheck já está habilitado no agregador. Rodar separadamente pode ser útil para investigar uma regra ou adotar somente essa ferramenta, mas no pipeline comum tende a duplicar trabalho.
Devo habilitar todos os linters do golangci-lint?
Não. Comece com poucos analisadores de baixo ruído e alto impacto. Regras subjetivas ou muito rígidas podem consumir mais tempo em supressões do que economizam em bugs evitados.
Como adotar lint em um projeto Go legado?
Corrija primeiro os defeitos de maior risco e use --new-from-rev=origin/main para impedir novas violações. Depois reduza o baseline em mudanças pequenas, sem misturar refatoração automática com features.
Posso usar //nolint em código Go?
Pode, desde que a exceção seja específica e documentada. Prefira //nolint:nome-do-linter // motivo. Revise essas supressões como qualquer outra decisão técnica.
Próximo passo
Adicione uma configuração mínima, rode golangci-lint run ./... localmente e escolha os cinco primeiros problemas que representam risco real. Depois integre o mesmo comando ao CI. Quando a saída estiver estável, expanda a política com cuidado — não por ansiedade.
Se você está preparando portfólio ou entrevista, saber explicar por que escolheu determinadas regras vale mais do que dizer que habilitou todos os linters. O guia de entrevista técnica Go em 2026 ajuda a conectar qualidade, testes e decisões de engenharia ao que times brasileiros esperam de profissionais Go.