Artigo 41 — Linting, formatação e análise estática: gofmt, golangci-lint
Curso: Dominando Go em 1 Ano Prof. Ricardo Matos Módulo 7 — Ferramentas, CLI e Qualidade
Qualidade de código automatizada: o computador como revisor implacável
Todo time de desenvolvimento acumula convenções não escritas que só existem na cabeça dos membros mais antigos. Tabs ou espaços? Onde vai o erro neste retorno? Esse lock está sendo usado corretamente? Análise estática responde a essas perguntas de forma consistente, para todos os contribuidores, em cada commit.
Go tem uma vantagem única nessa área: a linguagem foi projetada com ferramentas em mente. O compilador é rigoroso, o gofmt é universal e o ecossistema de análise estática é maduro. Integrar essas ferramentas ao fluxo de trabalho é simples e os ganhos em qualidade são imediatos.
gofmt: um único estilo para toda a comunidade
gofmt formata código Go de acordo com o estilo oficial da linguagem. Não existe configuração — não há como ajustar a indentação, a posição das chaves ou o espaçamento. Isso elimina completamente as discussões de estilo entre equipes.
# Formatar um arquivo
gofmt -w arquivo.go
# Formatar recursivamente todos os arquivos Go do projeto
gofmt -w .
# Ver o diff sem aplicar
gofmt -d arquivo.go
# Verificar se o arquivo está formatado (exit code 1 se não estiver)
gofmt -l .
gofmt também realiza simplificações automáticas com a flag -s:
# Simplifica construções redundantes
gofmt -s -w .
Exemplos de simplificações que -s aplica:
// Antes
s := []int{1, 2, 3}
for i, _ := range s { } // → for i := range s {}
x := []int{int(1), int(2)} // → x := []int{1, 2}
a = a[0:len(a)] // → a = a[:]
goimports: gofmt com gerenciamento de imports
goimports faz tudo que gofmt faz e ainda adiciona imports ausentes e remove imports não utilizados:
go install golang.org/x/tools/cmd/goimports@latest
goimports -w .
A maioria dos editores executa goimports automaticamente ao salvar — é um dos primeiros plugins a instalar ao configurar um ambiente Go.
go vet: análise estática da biblioteca padrão
go vet detecta erros comuns que o compilador não captura:
go vet ./...
Exemplos do que go vet detecta:
// Printf com argumento errado
fmt.Printf("%d", "texto") // vet: Printf format %d has wrong type string
// Mutex copiado
mu := sync.Mutex{}
outraMu := mu // vet: assignment copies lock value
// Unreachable code após return
func f() int {
return 1
fmt.Println("nunca") // vet: unreachable code
}
// String de formato inválida
fmt.Sprintf("%!") // vet: invalid format string
// Erro ignorado em defer
defer resp.Body.Close() // ok — mas com valor retornado ignorado
// Estrutura com tag JSON inválida
type T struct {
Campo string `json:"campo,omitemtempty"` // vet: typo in tag
}
staticcheck: análise estática avançada
staticcheck é o analisador estático mais abrangente do ecossistema Go, cobrindo dezenas de verificações que go vet não faz:
go install honnef.co/go/tools/cmd/staticcheck@latest
staticcheck ./...
// SA1000: expressão regular inválida
regexp.MustCompile("[invalid")
// SA1006: Printf sem argumentos
fmt.Printf("mensagem sem args\n") // use Println
// SA1019: uso de função depreciada
ioutil.ReadFile("arquivo") // use os.ReadFile
// SA4006: variável atribuída mas nunca usada
x := calcular()
return nil // x nunca foi usado
// SA9003: bloco if sempre verdadeiro
if true {
return
}
// S1000: select com apenas um case
select {
case v := <-ch:
usar(v)
}
// simplifique para: v := <-ch
// ST1003: nome não idiomático
func getHTTPClient() {} // correto: GetHTTPClient ou httpClient
golangci-lint: o orquestrador
golangci-lint executa múltiplos linters em paralelo de forma eficiente. É a ferramenta padrão da indústria para pipelines de CI/CD.
# Instalação
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
# Executar
golangci-lint run ./...
# Com tempo limite maior para projetos grandes
golangci-lint run --timeout 5m ./...
# Mostrar apenas erros novos em relação ao branch principal
golangci-lint run --new-from-rev=main ./...
Configuração do golangci-lint
O arquivo .golangci.yml na raiz do projeto configura quais linters rodar e suas opções:
# .golangci.yml
run:
timeout: 5m
tests: true
modules-download-mode: readonly
output:
format: colored-line-number
print-issued-lines: true
print-linter-name: true
linters:
disable-all: true
enable:
# Essenciais
- errcheck # verifica erros ignorados
- gosimple # sugere simplificações
- govet # análise estática padrão
- ineffassign # atribuições que nunca são usadas
- staticcheck # análise estática avançada
- unused # código não utilizado
# Estilo e idioma Go
- gofmt # formatação
- goimports # imports organizados
- misspell # erros de ortografia em comentários
- godot # ponto final em comentários
# Qualidade
- bodyclose # verifica fechamento de resp.Body
- contextcheck # uso correto de context
- errorlint # erros compatíveis com errors.Is/As
- exhaustive # switch em enums deve cobrir todos os casos
- gocritic # sugestões variadas de melhoria
- noctx # evita http.NewRequest sem context
- prealloc # pré-alocação de slices
- revive # linter de estilo configurável
- sqlclosecheck # verifica sql.Rows.Close()
- unparam # parâmetros que sempre recebem o mesmo valor
# Segurança
- gosec # verifica vulnerabilidades de segurança
- nilerr # retornar nil em vez de err
linters-settings:
errcheck:
check-type-assertions: true
check-blank: true
govet:
enable-all: true
gosec:
excludes:
- G104 # erros ignorados em defer (já coberto por errcheck)
revive:
rules:
- name: exported
arguments:
- checkPrivateReceivers
- sayRepetitiveInsteadOfStutters
gocritic:
enabled-tags:
- diagnostic
- performance
- style
misspell:
locale: US
goimports:
local-prefixes: github.com/minha-empresa
issues:
exclude-rules:
# Ignorar erros de lint em arquivos de teste
- path: _test\.go
linters:
- errcheck
- gosec
# Ignorar comentários sem ponto em código gerado
- path: \.pb\.go
linters:
- godot
- revive
max-issues-per-linter: 0
max-same-issues: 0
Os linters mais importantes
errcheck: erros nunca devem ser silenciados
// ERRADO — errcheck detecta
os.Remove("arquivo.tmp")
json.Unmarshal(dados, &v)
rows.Close()
// CORRETO
if err := os.Remove("arquivo.tmp"); err != nil {
log.Printf("aviso: remover temp: %v", err)
}
if err := json.Unmarshal(dados, &v); err != nil {
return fmt.Errorf("decodificar: %w", err)
}
defer func() {
if err := rows.Close(); err != nil {
log.Printf("fechar rows: %v", err)
}
}()
bodyclose: vazamentos de conexão HTTP
// ERRADO — bodyclose detecta
resp, _ := http.Get("https://exemplo.com")
dados, _ := io.ReadAll(resp.Body)
// CORRETO
resp, err := http.Get("https://exemplo.com")
if err != nil {
return err
}
defer resp.Body.Close()
dados, err := io.ReadAll(resp.Body)
errorlint: erros compatíveis com wrapping
// ERRADO — não funciona com erros embrulhados
if err == sql.ErrNoRows { }
if err == os.ErrNotExist { }
// CORRETO — errorlint reforça uso de errors.Is
if errors.Is(err, sql.ErrNoRows) { }
if errors.Is(err, os.ErrNotExist) { }
// ERRADO — type assertion direta
if e, ok := err.(*MyError); ok { }
// CORRETO
var e *MyError
if errors.As(err, &e) { }
gosec: segurança
// G101: credencial hardcoded
senha := "minha-senha-secreta" // gosec: hardcoded credential
// G304: abertura de arquivo com caminho do usuário
caminho := r.FormValue("arquivo")
os.Open(caminho) // gosec: file inclusion
// G401: algoritmo criptográfico fraco
md5.Sum(dados) // gosec: use sha256 ou superior
// G501: import de pacote inseguro
import "crypto/md5" // gosec
// Correto
import "crypto/sha256"
hash := sha256.Sum256(dados)
prealloc: performance com slices
// ERRADO — prealloc detecta
var resultado []string
for _, item := range itens {
resultado = append(resultado, processar(item))
}
// CORRETO
resultado := make([]string, 0, len(itens))
for _, item := range itens {
resultado = append(resultado, processar(item))
}
Integração com editores
VS Code
// .vscode/settings.json
{
"go.lintTool": "golangci-lint",
"go.lintFlags": ["--fast"],
"go.formatTool": "goimports",
"[go]": {
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": true
}
}
}
Makefile com targets de qualidade
# Makefile
.PHONY: lint fmt vet test ci
fmt:
goimports -w .
gofmt -s -w .
vet:
go vet ./...
lint:
golangci-lint run ./...
test:
go test -race -count=1 ./...
test-cover:
go test -race -count=1 -coverprofile=cobertura.out ./...
go tool cover -html=cobertura.out -o cobertura.html
ci: fmt vet lint test
@echo "✓ Todas as verificações passaram"
# Verifica se há arquivos não formatados (para CI)
check-fmt:
@unformatted=$$(gofmt -l .); \
if [ -n "$$unformatted" ]; then \
echo "Arquivos não formatados:"; \
echo "$$unformatted"; \
exit 1; \
fi
GitHub Actions: CI com qualidade automatizada
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
qualidade:
name: Qualidade de Código
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configurar Go
uses: actions/setup-go@v5
with:
go-version: '1.22'
cache: true
- name: Verificar formatação
run: |
unformatted=$(gofmt -l .)
if [ -n "$unformatted" ]; then
echo "Arquivos precisam de formatação:"
echo "$unformatted"
exit 1
fi
- name: go vet
run: go vet ./...
- name: golangci-lint
uses: golangci/golangci-lint-action@v4
with:
version: latest
args: --timeout=5m
- name: Testes com cobertura
run: |
go test -race -count=1 \
-coverprofile=cobertura.out \
-covermode=atomic \
./...
- name: Verificar cobertura mínima
run: |
cobertura=$(go tool cover -func=cobertura.out | \
grep total | awk '{print $3}' | sed 's/%//')
echo "Cobertura: ${cobertura}%"
if (( $(echo "$cobertura < 70" | bc -l) )); then
echo "Cobertura abaixo do mínimo de 70%"
exit 1
fi
- name: Upload de cobertura
uses: codecov/codecov-action@v4
with:
file: cobertura.out
Pre-commit hooks com lefthook
go install github.com/evilmartians/lefthook@latest
# lefthook.yml
pre-commit:
parallel: true
commands:
fmt:
glob: "*.go"
run: goimports -w {staged_files}
vet:
glob: "*.go"
run: go vet ./...
lint:
glob: "*.go"
run: golangci-lint run --new-from-rev=HEAD~1 {staged_files}
pre-push:
commands:
tests:
run: go test -race ./...
lefthook install # instala os hooks no repositório git
Baseline: adotando lint em projeto existente
Ao adotar golangci-lint em um projeto com muitos problemas existentes, a abordagem incremental é mais prática:
# Gera um baseline com todos os problemas atuais
golangci-lint run --out-format=json ./... > .golangci-baseline.json
# Nas execuções seguintes, reporta apenas novos problemas
golangci-lint run --baseline=.golangci-baseline.json ./...
Ou usando a flag --new-from-rev para reportar apenas problemas introduzidos após um commit específico:
# Reporta apenas problemas em código alterado desde o branch main
golangci-lint run --new-from-rev=origin/main ./...
Resumo do que foi coberto
Este artigo apresentou análise estática e qualidade de código em Go de forma completa: gofmt e goimports para formatação uniforme, go vet para detecção de erros comuns, staticcheck para análise avançada, configuração detalhada do golangci-lint com os linters mais importantes — errcheck, bodyclose, errorlint, gosec e prealloc — integração com VS Code, Makefile com targets de qualidade, pipeline de CI com GitHub Actions incluindo verificação de cobertura e pre-commit hooks com lefthook. O próximo artigo aborda documentação com godoc.
Referências e leituras complementares
-
golangci-lint — documentação oficial — Lista completa de linters e configuração. https://golangci-lint.run
-
staticcheck — Documentação de todas as verificações disponíveis. https://staticcheck.dev
-
gosec — Verificações de segurança para código Go. https://github.com/securego/gosec
-
Documentação gofmt — Referência e opções do formatador oficial. https://pkg.go.dev/cmd/gofmt
-
lefthook — Gerenciador de git hooks. https://github.com/evilmartians/lefthook
-
Go Code Review Comments — Guia oficial de boas práticas de código Go. https://github.com/golang/go/wiki/CodeReviewComments
Próximo artigo: Artigo 42 — Documentação com godoc e boas práticas de comentário**
you asked
Sim