Aller au contenu
Développement medium

Qualité du code Go : vet, staticcheck, golangci-lint

9 min de lecture

Go

La revue de code humaine ne devrait jamais porter sur un point-virgule mal placé ou une erreur ignorée : les linters attrapent ces défauts mécaniques en une seconde. Ce guide met en place la chaîne de qualité standard de Go : le formatage avec gofmt, la détection de bugs suspects avec go vet, puis l'agrégateur golangci-lint qui regroupe des dizaines d'analyses derrière une seule commande. On termine par le pre-commit et la CI. Public visé : intermédiaire en Go. Tous les exemples sont testés avec Go 1.26 et golangci-lint 2.12.

  • Formater automatiquement avec gofmt et goimports.
  • Détecter les bugs que le compilateur laisse passer avec go vet.
  • Agréger les linters avec golangci-lint et sa configuration.
  • Intégrer le lint en pre-commit et en CI.

Go tranche un débat que d'autres langages laissent ouvert : le formatage n'est pas une question de goût. L'outil gofmt, livré avec Go, impose un style unique. Tout le code Go du monde se ressemble, ce qui supprime les discussions de mise en forme en revue.

Fenêtre de terminal
# Lister les fichiers mal formatés (sans les modifier)
gofmt -l .
# Reformater en place tout le projet
gofmt -w .

gofmt -l renvoie la liste des fichiers non conformes : une sortie vide signifie que tout est formaté. En pratique, on utilise plutôt goimports, qui fait tout ce que fait gofmt et gère en plus les imports : il ajoute ceux qui manquent et retire ceux qui ne servent plus.

Fenêtre de terminal
go install golang.org/x/tools/cmd/goimports@latest
goimports -w .

go vet : les bugs que le compilateur laisse passer

Section intitulée « go vet : les bugs que le compilateur laisse passer »

Le compilateur Go refuse le code qui ne compile pas, mais il accepte du code qui compile et se trompe. go vet, lui aussi livré avec Go, repère une famille de bugs classiques : verbes de format incohérents, boucles mal capturées, structures mal taguées.

Prenons un fichier volontairement fautif :

main.go
package main
import (
"fmt"
"os"
)
func main() {
// verbe %d avec une chaîne de caractères
fmt.Printf("port = %d\n", "8080")
// l'erreur renvoyée par os.Setenv est ignorée
os.Setenv("APP_ENV", "prod")
// la première affectation est écrasée sans être lue
msg := "bonjour"
msg = "au revoir"
fmt.Println(msg)
}

Ce code compile sans broncher. go vet détecte pourtant l'incohérence de format :

Fenêtre de terminal
go vet ./...
main.go:10:21: fmt.Printf format %d has arg "8080" of wrong type string

go vet attrape le %d appliqué à une string, mais il ne dit rien de l'erreur ignorée ni de l'affectation inutile. Pour ces cas, il faut aller plus loin.

go vet ne couvre qu'une partie du terrain. D'autres analyseurs existent, chacun spécialisé : errcheck pour les erreurs non vérifiées, ineffassign pour les affectations mortes, staticcheck pour un large éventail de simplifications et de bugs subtils. Les lancer un par un serait fastidieux. golangci-lint les exécute tous en parallèle, dédoublonne les résultats et présente un rapport unique.

Sur le même fichier fautif, il remonte les trois problèmes d'un coup :

Fenêtre de terminal
golangci-lint run ./...
main.go:13:11: Error return value of `os.Setenv` is not checked (errcheck)
os.Setenv("APP_ENV", "prod")
^
main.go:10:21: printf: fmt.Printf format %d has arg "8080" of wrong type string (govet)
fmt.Printf("port = %d\n", "8080")
^
main.go:16:2: ineffectual assignment to msg (ineffassign)
msg := "bonjour"
^
3 issues:
* errcheck: 1
* govet: 1
* ineffassign: 1

Chaque ligne indique le linter responsable entre parenthèses. La commande sort en code d'erreur non nul dès qu'un problème est trouvé, ce qui la rend directement exploitable en CI.

Par défaut, golangci-lint active un jeu standard raisonnable. Vous ajustez la sélection dans un fichier .golangci.yml à la racine du projet. Depuis la version 2, il commence par la clé version: "2" :

.golangci.yml
version: "2"
linters:
default: standard
enable:
- errcheck
- govet
- ineffassign
- staticcheck
- unused
- misspell

Vérifiez que votre configuration est valide avant de la committer :

Fenêtre de terminal
golangci-lint config verify

Une sortie vide confirme que le fichier est correct. default: standard conserve le jeu par défaut, et enable y ajoute des linters, ici misspell qui traque les fautes d'orthographe dans les commentaires et chaînes.

Un linter n'a de valeur que s'il tourne systématiquement. Deux points de passage le garantissent : avant chaque commit, en local, et à chaque push, en CI.

Un hook Git bloque le commit si le code n'est pas propre. Créez .git/hooks/pre-commit, rendez-le exécutable :

.git/hooks/pre-commit
#!/bin/sh
set -e
gofmt -l . | grep . && { echo "Fichiers mal formatés, lancez gofmt -w ."; exit 1; }
go vet ./...
golangci-lint run ./...

Le développeur reçoit le retour avant de pousser, quand la correction coûte le moins cher.

En CI, faites échouer le job dès qu'un linter proteste. Voici un workflow GitHub Actions durci, qui s'appuie sur l'image conteneur officielle épinglée par version :

.github/workflows/lint.yml
name: lint
on: [push, pull_request]
permissions: {}
jobs:
lint:
runs-on: ubuntu-latest
container: golangci/golangci-lint:v2.12.2@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- run: golangci-lint run ./...

Les garde-fous à retenir : permissions: {} retire tous les droits par défaut au workflow, persist-credentials: false évite de laisser traîner le jeton Git dans le runner, l'action checkout est épinglée par SHA plutôt que par tag mouvant, et l'image conteneur est épinglée par digest @sha256 (un tag comme v2.12.2 peut être redéployé sur un autre contenu). Ce workflow passe les scanners de pipeline zizmor, poutine et plumber sans alerte. Le guide GitHub Actions détaille cette approche de durcissement.

  • Formatez à la sauvegarde avec goimports : ne committez jamais un fichier mal formaté.
  • Lancez go vet et golangci-lint en local avant de pousser, via un hook pre-commit.
  • Versionnez .golangci.yml : toute l'équipe partage la même définition de « code propre ».
  • Épinglez la version de golangci-lint (image ou binaire) : un lint reproductible ne casse pas au gré des mises à jour.
  • Traitez un avertissement de linter comme une erreur en CI : le job échoue, la régression n'entre pas.
  • Le formatage Go n'est pas négociable : gofmt/goimports imposent un style unique.
  • go vet attrape des bugs que le compilateur accepte, mais son périmètre est limité.
  • golangci-lint agrège de nombreux linters (errcheck, ineffassign, staticcheck) en une commande.
  • Un .golangci.yml versionné fige la définition de qualité pour toute l'équipe.
  • Le lint prend sa valeur en pre-commit et en CI, où il bloque toute régression.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn