Stack de Desenvolvimento Go
Configuração opinativa e moderna para desenvolvimento Go. Uma ferramenta por responsabilidade, zero sobreposição.
Quando Usar
- Iniciar um novo projeto Go do zero
- Adicionar infraestrutura de linting, formatação ou testes
- Configurar CI/CD para um serviço ou biblioteca Go
- Criar um Justfile para substituir um Makefile
- Adicionar ferramentas de migração de banco de dados
- Migrar de invocações dispersas de gofmt/govet/staticcheck para uma configuração unificada
Não use para forks que fazem merge regular do upstream: substituir o Makefile e reformatar a árvore com gofumpt causa conflitos em cada merge e enterra mudanças reais em um diff de reformatação. Mantenha o gofmt e as ferramentas de build do upstream e adicione apenas verificações.
O Stack
| Ferramenta | Versão | Função | Substitui |
|---|---|---|---|
| Go | 1.27+ | Linguagem, toolchain, go mod, go fix | - |
| golangci-lint | v2.14+ | Meta-linter (100+ linters + formatadores + comando fmt) | gofmt, govet, staticcheck, errcheck executados separadamente |
| gofumpt | v0.12+ | Formatador rigoroso (superconjunto de gofmt, 19 regras padrão) | gofmt |
| gotestsum | v1.13+ | Executor de testes com saída legível, modo watch, XML JUnit | go test puro |
| just | 1.58+ | Executor de tarefas | Makefile |
| golang-migrate | v4.20+ | Migrações de BD (CLI + biblioteca + embed.FS) | Scripts SQL manuais |
| lefthook | v2.1+ | Hooks Git (binário único, paralelo) | pre-commit (Python) |
Versões mínimas (floors) são importantes. golangci-lint "suporta versões do Go iguais ou inferiores à versão do Go usada para compilá-lo" - uma versão mais antiga que seu toolchain Go falha diretamente. O suporte a Go 1.27 chegou no golangci-lint v2.13.0, então v2.13 é o mínimo para um projeto Go 1.27; esta skill usa v2.14.0, a primeira versão que inclui gofumpt v0.12.0. Mais dois floors foram movidos recentemente: gofumpt v0.12.0 "é baseado no gofmt do Go 1.27 e requer Go 1.26 ou superior", e o caminho go install do lefthook agora pede Go 1.26+.
Início Rápido: Novo Projeto
# 1. Criar módulo
mkdir myapp && cd myapp
go mod init github.com/yourorg/myapp
# 2. Criar diretórios
mkdir -p cmd/myapp internal migrations
# 3. Instalar golangci-lint como binário, não como ferramenta de módulo (veja nota abaixo)
curl -sSfL https://golangci-lint.run/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.14.0
# 4. Rastrear o resto no go.mod (diretiva tool Go 1.24+). Fixe versões - nunca @latest,
# que recompila a ferramenta a cada execução de CI e varia entre máquinas.
go get -tool mvdan.cc/[email protected]
go get -tool gotest.tools/[email protected]
# golang-migrate precisa de uma build tag, então instale diretamente
go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/[email protected]
# 5. Criar arquivos de configuração (modelos abaixo)
# 6. Executar: just check
Não instale o golangci-lint através do padrão de ferramentas. O upstream é explícito: "Usar go install/go get, "tools pattern" e comando/diretivas tool não são garantidos de funcionar. Recomendamos a instalação binária." O motivo que importa em um repositório compartilhado é o vazamento de dependências - "as dependências de uma ferramenta podem modificar as dependências de outra ferramenta ou do seu projeto". Se você precisar ter no go.mod, isole-o atrás de seu próprio -modfile - veja a Referência golangci-lint.
go get -tool rastreia; go tool executa. A diretiva tool registra a dependência no go.mod mas não coloca nada no seu PATH. Invoque através do toolchain - go tool gofumpt -l ., go tool gotestsum --format testname - ou execute go install tool uma vez para popular $(go env GOPATH)/bin. O Justfile abaixo chama os binários diretamente, então assume o caminho go install tool (ou uma instalação de sistema via Homebrew - mas o "Homebrew pode usar uma versão inesperada do Go para construir o binário" para golangci-lint, e não pode fixar uma versão). Note que go tool resolve em relação ao módulo no diretório atual - "ferramentas adicionais podem ser definidas no go.mod do módulo atual" - então em um monorepo ele falha com go: no such tool "..." a menos que a receita defina [working-directory(...)].
Dois comportamentos do comando Go que vale a pena conhecer antes do primeiro commit:
go mod initsob um toolchain 1.N escrevego 1.(N-1).0, não1.N- "Executargo mod initusando um toolchain de versão1.N.Xcriará um arquivogo.modespecificando a versão do Gogo 1.(N-1).0." Aumente a diretiva deliberadamente se quiser recursos de linguagem 1.N.- Defina uma linha
toolchain go1.27.1nogo.mod, mas saiba que é um mínimo, não uma fixação: sob o padrãoGOTOOLCHAIN=auto, o comandogomuda apenas "se ...<tname>for mais novo que o toolchain Go padrão", então umgolocal mais novo vence. Para um toolchain exato, definaGOTOOLCHAIN=go1.27.1(CI ougo env -w);GODEBUG=toolchaintrace=1 go versionmostra qual foi selecionado. Mantenha a linha no patch atual, não no .0: ele seleciona ogocontra o qualgovulncheckverifica os avisos da stdlib em qualquer máquina cujo própriogoseja mais antigo, então um patch desatualizado acende o alerta vermelho no CI por conta própria - veja Armadilhas abaixo.
.golangci.yml
version: "2"
run:
timeout: 5m
build-tags:
- integration # caso contrário, arquivos atrás da tag integration do Justfile nunca são lintados
linters:
default: standard
enable:
- bodyclose
- copyloopvar
- dupl
- durationcheck
- err113
- errname
- errorlint
- exhaustive
- exptostd
- fatcontext
- goconst
- gocritic
- gosec
- intrange
- misspell
- modernize
- musttag
- nakedret
- nestif
- nilerr
- noctx
- nolintlint
- nonamedreturns
- perfsprint
- prealloc
- revive
- sqlclosecheck
- testifylint
- thelper
- unconvert
- unparam
- usestdlibvars
- usetesting
- wastedassign
- whitespace
- wrapcheck
settings:
govet:
enable:
- shadow
gocritic:
enabled-checks:
- nestingReduce
revive:
enable-all-rules: true
rules:
# enable-all-rules ativa `unhandled-error`, que sinaliza `fmt.Println` no main.
# Sob enable-all-rules, os `arguments` de uma regra são ignorados (a regra registra
# duas vezes), então uma lista de permissões não funciona aqui - apenas `disabled` tem efeito.
- name: unhandled-error
disabled: true
errcheck:
check-type-assertions: true
exclusions:
generated: strict
presets:
- comments
- std-error-handling
- common-false-positives
rules:
- path: _test\.go
linters:
- errcheck
- dupl
- gosec
- wrapcheck
formatters:
enable:
- gofumpt
- goimports
settings:
gofumpt:
# Selecione as regras individualmente. `extra-rules: true` está obsoleto e também
# ativa `balance_calls`, que o próprio gofumpt rebaixou como controverso.
extra:
group-params: true
clothe-returns: true
balance-calls: false
exclusions:
generated: strict
paths:
- vendor/
output:
formats:
text:
path: stdout
print-linter-name: true
colors: true
sort-order:
- linter
- file
show-stats: true
Justfile
set shell := ["bash", "-euo", "pipefail", "-c"]
set dotenv-load
binary := "myapp"
[private]
default:
@just --list --unsorted
# ── Qualidade de Código ────────────────────────────────────
# Formatar todo o código Go
[group('quality')]
fmt:
golangci-lint fmt ./...
# Verificar formatação sem modificar (seguro para CI)
[group('quality')]
fmt-check:
golangci-lint fmt --diff ./...
# Executar linter
[group('quality')]
lint:
golangci-lint run ./...
# Executar linter com correção automática
[group('quality')]
lint-fix:
golangci-lint run --fix ./...
# Executar verificação de vulnerabilidades
[group('quality')]
vuln:
govulncheck ./...
# ── Testes ─────────────────────────────────────────────────
# Executar todos os testes com detecção de race condition
[group('test')]
test *args="./...":
gotestsum --format testname -- -race {{ args }}
# Executar testes com cobertura
[group('test')]
test-cov:
gotestsum --format testname -- -race -coverprofile=coverage.out -covermode=atomic ./...
go tool cover -func=coverage.out
# Abrir relatório de cobertura no navegador
[group('test')]
coverage: test-cov
go tool cover -html=coverage.out
# Executar testes de integração
[group('test')]
test-integration:
gotestsum --format testname -- -race -tags=integration ./...
# Observar testes durante o desenvolvimento
[group('test')]
test-watch:
gotestsum --watch --watch-clear --format testname
# Executar benchmarks
[group('test')]
bench:
go test -bench=. -benchmem ./...
# ── Build ──────────────────────────────────────────────────
# Compilar o binário
[group('build')]
build:
go build -o {{ binary }} ./cmd/{{ binary }}
# Compilar binário de release otimizado
[group('build')]
build-release:
CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o {{ binary }} ./cmd/{{ binary }}
# ── Dependências ───────────────────────────────────────────
# Organizar e verificar módulos
[group('deps')]
tidy:
go mod tidy
go mod verify
# Falhar se go.mod/go.sum estiverem desorganizados, sem tocá-los (seguro para CI)
[group('deps')]
tidy-check:
go mod tidy -diff
# Executar geradores de código
[group('deps')]
generate:
go generate ./...
# ── Banco de Dados ─────────────────────────────────────────
# Aplicar todas as migrações pendentes
[group('db')]
migrate-up:
migrate -path migrations -database "$DATABASE_URL" up
# Reverter a última migração
[group('db')]
migrate-down:
migrate -path migrations -database "$DATABASE_URL" down 1
# Criar uma nova migração
[group('db')]
migrate-create name:
migrate create -ext sql -dir migrations -seq {{ name }}
# ── CI ─────────────────────────────────────────────────────
# Portão completo de CI (verificação de formatação + lint + testes)
[group('ci')]
check: fmt-check lint test
@echo "Todas as verificações passaram"
# Limpar artefatos de build
[group('ci')]
clean:
go clean
rm -f {{ binary }} coverage.out
check deliberadamente deixa de fora vuln: govulncheck chama a rede, e uma atualização do banco de vulnerabilidades pode tornar o portão vermelho em uma alteração que não tocou em nada. Execute-o como seu próprio job de CI. golangci-lint run também executa os formatadores habilitados e relata seus problemas, então fmt-check duplica essa verificação - mantenha-o mesmo assim pelo diff legível.
Configuração do Lefthook
Lefthook é preferido ao pre-commit para projetos Go - é um único binário Go, executa hooks em paralelo e não precisa de Python.
go install github.com/evilmartians/lefthook/[email protected] # precisa de Go 1.26+
lefthook install
# lefthook.yml
assert_lefthook_installed: true # falhar alto em vez de pular todas as regras
pre-commit:
piped: true # falhe rápido - pare no primeiro job com falha
commands:
fmt:
glob: "*.go"
run: golangci-lint fmt {staged_files}
stage_fixed: true
lint:
glob: "*.go"
# Nunca passe uma lista de arquivos nua para `golangci-lint run`: uma lista abrangendo
# dois diretórios é rejeitada diretamente, e um arquivo de um pacote com vários arquivos
# relata erros de typecheck fantasma `undefined:`. Lint os pacotes.
run: printf '%s\n' {staged_files} | xargs -n1 dirname | sort -u | xargs golangci-lint run --fix
stage_fixed: true
mod-tidy:
glob: "*.{go,mod,sum}"
run: go mod tidy
pre-push:
commands:
test:
run: go test -race ./...
piped: true é falha rápida, não ordenação - lefthook "executa comandos e scripts sequencialmente por padrão", e piped adiciona "Pare de executar comandos e scripts se um deles falhar." Não pode ser combinado com parallel: true.
jobs: (adicionado no lefthook 1.10.0) é a primitiva mais nova ao lado da divisão commands:/scripts: - "Jobs fornecem uma maneira flexível de definir tarefas, suportando tanto comandos quanto scripts. Jobs podem ser agrupados para controle de fluxo avançado." commands: não está obsoleto e permanece totalmente documentado; recorra a jobs: quando precisar de agrupamento, controle de fluxo aninhado ou uma mistura de comandos e scripts em um único hook.
Mais quatro que valem a pena configurar:
assert_lefthook_installed: true, acima, é o antídoto para a armadilha de inatividade abaixo: "falha (com código de saída 1) se o executávellefthooknão for encontrado no $PATH".lefthook validateno CI detecta umlefthook.ymlmalformado antes que ele desabilite silenciosamente os hooks;lefthook dumpimprime a configuração mesclada efetiva quando um hook não se comporta como escrito.- Um
lefthook-local.ymlignorado pelo git permite que um desenvolvedor adicione ou pule jobs sem impô-los aos colegas - "Isso é útil quando você quer usar lefthook localmente sem impô-lo aos seus colegas." - Em um monorepo, dê a cada job um
root:apontando para seu diretório de módulo; sem ele,go mod tidyego toolexecutam contra a raiz do repositório e falham. skip: [merge, rebase]nos jobsmod-tidye lint os mantém fora de commits feitos durante um merge ou rebase em andamento, para que resolver conflitos não também reestage uma reorganização tidy ou edições--fix.
A suíte pre-push repete o que o CI já executa. Quando levar minutos, remova-a ou escopá-a para pacotes alterados - caso contrário, os desenvolvedores aprendem a fazer push com LEFTHOOK=0. Testes executados a partir de um hook também herdam o ambiente de hook do git (GIT_DIR, GIT_INDEX_FILE, GIT_WORK_TREE), então um helper de teste que executa git init ou git commit em t.TempDir() age no repositório real - limpe essas variáveis em cmd.Env.
Um git merge sem conflitos nunca executa pre-commit: o git invoca pre-merge-commit em vez disso, e executa pre-commit apenas quando você finaliza um merge com conflitos com git commit. Espelhe as verificações sob uma chave pre-merge-commit:, ou merges chegarão sem lint.
Comandos de hook resolvem ferramentas a partir do PATH do chamador, então um binário ausente falha o commit com um exit 127 puro. Executá-los como go tool <nome> (diretiva tool) torna o hook tão reproduzível quanto o build.
Beta, mas vale a pena saber: ai: declara hooks de agente LLM no mesmo arquivo - "Durante lefthook install, o lefthook gera o arquivo de configurações específico do provedor para que o agente chame lefthook run <hook> quando necessário."
[truncado para tradução]