Pile de développement Go
Configuration Go moderne et opinionée. Un outil par préoccupation, zéro chevauchement.
Quand l'utiliser
- Démarrer un nouveau projet Go à partir de zéro
- Ajouter une infrastructure de lint, formatage ou tests
- Mettre en place du CI/CD pour un service ou une bibliothèque Go
- Créer un Justfile pour remplacer un Makefile
- Ajouter des outils de migration de base de données
- Migrer d'invocations dispersées de gofmt/govet/staticcheck vers une configuration unifiée
Pas pour un fork qui fusionne régulièrement avec l'amont : remplacer son Makefile et reformater l'arborescence avec gofumpt entre en conflit à chaque fusion et enterre les vrais changements dans un diff de reformatage. Conservez le gofmt et les outils de construction de l'amont, et ajoutez uniquement des vérifications.
La pile
| Outil | Version | Rôle | Remplace |
|---|---|---|---|
| Go | 1.27+ | Langage, chaîne d'outils, go mod, go fix | - |
| golangci-lint | v2.14+ | Méta-linter (100+ linters + formateurs + commande fmt) | gofmt, govet, staticcheck, errcheck exécutés séparément |
| gofumpt | v0.12+ | Formateur strict (sur-ensemble de gofmt, 19 règles par défaut) | gofmt |
| gotestsum | v1.13+ | Exécuteur de tests avec sortie lisible, mode watch, XML JUnit | go test brut |
| just | 1.58+ | Lanceur de tâches | Makefile |
| golang-migrate | v4.20+ | Migrations de base de données (CLI + bibliothèque + embed.FS) | Scripts SQL manuels |
| lefthook | v2.1+ | Hooks Git (binaire unique, parallèle) | pre-commit (Python) |
Les versions minimales sont contraignantes. golangci-lint « prend en charge les versions de Go inférieures ou égales à la version de Go utilisée pour le compiler » - une version antérieure à votre chaîne d'outils Go échoue complètement. La prise en charge de Go 1.27 est arrivée dans golangci-lint v2.13.0, donc v2.13 est le minimum pour un projet Go 1.27 ; ce skill fixe v2.14.0, la première version qui intègre gofumpt v0.12.0. Deux autres versions minimales ont récemment bougé : gofumpt v0.12.0 « est basé sur gofmt de Go 1.27 et nécessite Go 1.26 ou plus récent », et le chemin d'installation go install de lefthook nécessite maintenant Go 1.26+.
Démarrage rapide : Nouveau projet
# 1. Créer le module
mkdir myapp && cd myapp
go mod init github.com/yourorg/myapp
# 2. Créer les répertoires
mkdir -p cmd/myapp internal migrations
# 3. Installer golangci-lint en tant que binaire, pas en tant qu'outil de module (voir note ci-dessous)
curl -sSfL https://golangci-lint.run/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.14.0
# 4. Enregistrer le reste dans go.mod (directive d'outil Go 1.24+). Versionnez explicitement - jamais @latest,
# qui recompile l'outil à chaque exécution CI et dérive entre les machines.
go get -tool mvdan.cc/[email protected]
go get -tool gotest.tools/[email protected]
# golang-migrate nécessite une balise de construction, donc installez-le directement
go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/[email protected]
# 5. Créer les fichiers de configuration (modèles ci-dessous)
# 6. Exécuter : just check
N'installez pas golangci-lint via le modèle d'outils. L'amont est explicite : « L'utilisation de go install/go get, du "modèle d'outils", et des commandes/directives tool n'est pas garantie de fonctionner. Nous recommandons une installation binaire. » La raison pour laquelle cela compte dans un dépôt partagé est la contamination des dépendances - « les dépendances d'un outil peuvent modifier les dépendances d'un autre outil ou de votre projet ». Si vous devez l'avoir dans go.mod, isolez-le derrière son propre -modfile - voir la Référence golangci-lint.
go get -tool trace ; go tool exécute. La directive d'outil enregistre la dépendance dans go.mod mais ne met rien dans votre PATH. Invoquez soit via la chaîne d'outils - go tool gofumpt -l ., go tool gotestsum --format testname - soit go install tool une fois pour remplir $(go env GOPATH)/bin. Le Justfile ci-dessous appelle les binaires nus, donc il suppose la route go install tool (ou une installation système via Homebrew - mais « Homebrew peut utiliser une version inattendue de Go pour construire le binaire » pour golangci-lint, et il ne peut pas épingler une version). Notez que go tool résout par rapport au module dans le répertoire courant - « des outils supplémentaires peuvent être définis dans le go.mod du module courant » - donc dans un monorepo, il échoue avec go: no such tool "..." sauf si la recette définit [working-directory(...)].
Deux comportements de la commande Go à connaître avant le premier commit :
go mod initsous une chaîne d'outils 1.N écritgo 1.(N-1).0, pas1.N- « Exécutergo mod initen utilisant une chaîne d'outils de version1.N.Xcréera un fichiergo.modspécifiant la version de Gogo 1.(N-1).0. » Augmentez la directive délibérément si vous voulez des fonctionnalités de langage 1.N.- Définissez une ligne
toolchain go1.27.1dansgo.mod, mais sachez qu'il s'agit d'un minimum, pas d'un verrouillage : sous la valeur par défautGOTOOLCHAIN=auto, la commandegone change que « si ...<tname>est plus récent que la chaîne d'outils Go par défaut », donc ungolocal plus récent gagne. Pour une chaîne d'outils exacte, définissezGOTOOLCHAIN=go1.27.1(CI ougo env -w) ;GODEBUG=toolchaintrace=1 go versionmontre laquelle a été choisie. Gardez la ligne à la version de correctif actuelle, pas la.0: elle sélectionne legocontre lequelgovulncheckanalyse les avis de la bibliothèque standard sur toute machine dont le propregoest plus ancien, donc un correctif obsolète échoue le CI tout seul - voir les Pièges ci-dessous.
.golangci.yml
version: "2"
run:
timeout: 5m
build-tags:
- integration # sinon les fichiers derrière la balise d'intégration du Justfile ne sont jamais lintés
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 active `unhandled-error`, qui signale `fmt.Println` dans main.
# Sous enable-all-rules, les `arguments` d'une règle sont ignorés (la règle s'enregistre
# deux fois), donc une liste d'autorisation ne fonctionne pas ici - seul `disabled` prend effet.
- 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:
# Sélectionnez les règles individuellement. `extra-rules: true` est obsolète, et il active
# aussi `balance_calls`, que gofumpt lui-même a rétrogradé comme controversé.
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
# ── Qualité du code ──────────────────────────────────────────
# Formater tout le code Go
[group('quality')]
fmt:
golangci-lint fmt ./...
# Vérifier le formatage sans modifier (sûr pour le CI)
[group('quality')]
fmt-check:
golangci-lint fmt --diff ./...
# Exécuter le linter
[group('quality')]
lint:
golangci-lint run ./...
# Exécuter le linter avec correction automatique
[group('quality')]
lint-fix:
golangci-lint run --fix ./...
# Exécuter la vérification de vulnérabilité
[group('quality')]
vuln:
govulncheck ./...
# ── Tests ───────────────────────────────────────────────
# Exécuter tous les tests avec détection de course
[group('test')]
test *args="./...":
gotestsum --format testname -- -race {{ args }}
# Exécuter les tests avec couverture
[group('test')]
test-cov:
gotestsum --format testname -- -race -coverprofile=coverage.out -covermode=atomic ./...
go tool cover -func=coverage.out
# Ouvrir le rapport de couverture dans le navigateur
[group('test')]
coverage: test-cov
go tool cover -html=coverage.out
# Exécuter les tests d'intégration
[group('test')]
test-integration:
gotestsum --format testname -- -race -tags=integration ./...
# Surveiller les tests pendant le développement
[group('test')]
test-watch:
gotestsum --watch --watch-clear --format testname
# Exécuter les benchmarks
[group('test')]
bench:
go test -bench=. -benchmem ./...
# ── Construction ─────────────────────────────────────────────────
# Construire le binaire
[group('build')]
build:
go build -o {{ binary }} ./cmd/{{ binary }}
# Construire le binaire de release optimisé
[group('build')]
build-release:
CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o {{ binary }} ./cmd/{{ binary }}
# ── Dépendances ──────────────────────────────────────────
# Nettoyer et vérifier les modules
[group('deps')]
tidy:
go mod tidy
go mod verify
# Échouer si go.mod/go.sum ne sont pas propres, sans les toucher (sûr pour le CI)
[group('deps')]
tidy-check:
go mod tidy -diff
# Exécuter les générateurs de code
[group('deps')]
generate:
go generate ./...
# ── Base de données ──────────────────────────────────────────────
# Appliquer toutes les migrations en attente
[group('db')]
migrate-up:
migrate -path migrations -database "$DATABASE_URL" up
# Revenir à la dernière migration
[group('db')]
migrate-down:
migrate -path migrations -database "$DATABASE_URL" down 1
# Créer une nouvelle migration
[group('db')]
migrate-create name:
migrate create -ext sql -dir migrations -seq {{ name }}
# ── CI ────────────────────────────────────────────────────
# Porte CI complète (vérification du formatage + lint + tests)
[group('ci')]
check: fmt-check lint test
@echo "Toutes les vérifications ont réussi"
# Nettoyer les artefacts de construction
[group('ci')]
clean:
go clean
rm -f {{ binary }} coverage.out
check omet délibérément vuln : govulncheck appelle le réseau, et une mise à jour de la base de vulnérabilités peut faire devenir la porte rouge sur un changement qui n'a rien touché. Exécutez-le comme son propre job CI à la place. golangci-lint run exécute également les formateurs activés et signale leurs problèmes, donc fmt-check duplique cette vérification - gardez-le quand même pour le diff lisible.
Configuration Lefthook
Lefthook est préféré à pre-commit pour les projets Go - c'est un binaire Go unique, exécute les hooks en parallèle, et ne nécessite pas Python.
go install github.com/evilmartians/lefthook/[email protected] # nécessite Go 1.26+
lefthook install
# lefthook.yml
assert_lefthook_installed: true # échouer bruyamment au lieu de sauter toutes les règles
pre-commit:
piped: true # échouer rapidement - s'arrêter au premier job qui échoue
commands:
fmt:
glob: "*.go"
run: golangci-lint fmt {staged_files}
stage_fixed: true
lint:
glob: "*.go"
# Ne jamais passer une simple liste de fichiers à `golangci-lint run` : une liste s'étendant
# sur deux répertoires est rejetée immédiatement, et un fichier d'un package multi-fichier
# signale des erreurs de type fantômes `undefined:`. Linter les packages à la place.
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 est un échec rapide, pas un ordre - lefthook « exécute les commandes et les scripts séquentiellement par défaut », et piped ajoute « Arrêter l'exécution des commandes et des scripts si l'un d'eux échoue ». Il ne peut pas être combiné avec parallel: true.
jobs: (ajouté dans lefthook 1.10.0) est la primitive plus récente à côté de la division commands:/scripts: - « Les jobs offrent un moyen flexible de définir des tâches, prenant en charge à la fois les commandes et les scripts. Les jobs peuvent être regroupés pour un contrôle de flux avancé. » commands: n'est pas obsolète et reste entièrement documenté ; utilisez jobs: lorsque vous avez besoin de regroupement, de contrôle de flux imbriqué, ou d'un mélange de commandes en ligne et de scripts dans un même hook.
Quatre autres éléments à câbler :
assert_lefthook_installed: true, ci-dessus, est l'antidote au piège de dormance ci-dessous : « échouer (avec un code de sortie 1) si l'exécutablelefthookne peut pas être trouvé dans $PATH ».lefthook validatedans le CI attrape unlefthook.ymlmal formé avant qu'il ne désactive silencieusement les hooks ;lefthook dumpaffiche la configuration effective fusionnée lorsqu'un hook ne se comporte pas comme écrit.- Un fichier
lefthook-local.ymlignoré par git permet à un développeur d'ajouter ou de sauter des jobs sans l'imposer à ses coéquipiers - « Ceci est utile lorsque vous souhaitez utiliser lefthook localement sans l'imposer à vos coéquipiers. » - Dans un monorepo, donnez à chaque job un
root:pointant vers son répertoire de module ; sans cela,go mod tidyetgo tools'exécutent contre la racine du dépôt et échouent. skip: [merge, rebase]sur les jobsmod-tidyet lint les maintient en dehors des commits effectués pendant une fusion ou un rebase en cours, de sorte que la résolution des conflits ne remette pas en scène une réécriture tidy ou des modifications--fix.
La suite de tests pre-push répète ce que le CI exécute déjà. Une fois qu'elle prend des minutes, supprimez-la ou limitez-la aux packages modifiés - sinon les développeurs apprennent à pousser avec LEFTHOOK=0. Les tests exécutés depuis un hook héritent également de l'environnement de hook de git (GIT_DIR, GIT_INDEX_FILE, GIT_WORK_TREE), donc un helper de test qui exécute git init ou git commit dans t.TempDir() agit sur le dépôt réel - effacez ces variables dans cmd.Env.
Une git merge sans conflit n'exécute jamais pre-commit : git invoque pre-merge-commit à la place, et exécute pre-commit seulement lorsque vous terminez une fusion conflictuelle avec git commit. Miroirez les vérifications sous une clé pre-merge-commit:, ou les fusions arriveront non lintées.
Les commandes de hook résolvent les outils à partir du PATH de l'appelant, donc un binaire manquant fait échouer le commit avec un simple exit 127. Les exécuter sous forme de go tool <nom> (directive d'outil) rend le hook aussi reproductible que la construction.
Bêta, mais bon à savoir : ai: déclare des hooks d'agent LLM dans le même fichier - « Pendant lefthook install, lefthook génère le fichier de paramètres spécifique au fournisseur afin que l'agent appelle lefthook run <hook> lorsque l'…
[Document tronqué pour la traduction]