🟩 Go — Guide PRO — Backend • CLI • Concurrency • Cloud
Guide HTML ultra détaillé sur Go : syntaxe, modules, goroutines, channels, net/http, tests, perf, observabilité, sécurité, Docker, gRPC et patterns de production.
Vue d’ensemble
Philosophie Go, cas d’usage, points forts, limites, ecosystem map.
MindsetUse casesInstallation & Toolchain
go install, GOROOT/GOPATH, modules, workspace, lint, IDE, build.
go envModulesSyntaxe essentielle
Variables, types, fonctions, pointeurs, control flow, switch, defer.
BasicsPointersStructs & Interfaces
Méthodes, composition, interfaces implicites, embedding, design idiomatique.
StructInterfaceGestion des erreurs
error wrapping, sentinel errors, errors.Is/As, panic/recover, patterns robustes.
errorrecoverModules & Architecture
go.mod, versioning, internal, cmd/pkg, monorepo, workspaces.
go.modLayoutGoroutines & Channels
fan-out/fan-in, pipelines, select, backpressure, worker pools.
GoroutinesChannelsContext & Cancellation
Deadlines, propagation, shutdown propre, request-scoped values.
contextTimeoutHTTP & APIs REST
net/http, middleware, JSON, validation, auth, handlers, timeouts.
RESTJSONBase de données
database/sql, transactions, sqlx, GORM, migrations, pool tuning.
SQLPoolTests & Qualité
testing, table-driven tests, fuzzing, bench, race detector, coverage.
TestsRaceCLI & Automation
cobra, flags, config, logs, outils internes, jobs cron, utilitaires.
CLIOpsgRPC & Messaging
protobuf, streaming, clients robustes, retries, brokers, event-driven.
gRPCProtoLogs & Observabilité
slog, metrics, tracing, pprof, OpenTelemetry, health checks.
slogOTelPerformance & Mémoire
allocations, escape analysis, GC, profiling CPU/memory, hotspots.
pprofGCSécurité & Production
Secrets, TLS, SSRF, injections, supply chain, hardening runtime.
TLSHardeningBuild, Docker & Deploy
multi-stage builds, static binaries, distroless, Kubernetes, CI/CD.
DockerK8sPatterns & Anti‑patterns
Clean architecture, service layer, repository, pragmatic Go, erreurs fréquentes.
PatternsAnti‑patternsGo offre une chaîne de build extrêmement efficace pour la production : compilation rapide, binaires simples à distribuer, cross-compilation native, images Docker compactes, forte compatibilité avec les conteneurs et excellente intégration dans les pipelines CI/CD. Le grand avantage n’est pas seulement la performance d’exécution ; c’est aussi la sobriété opérationnelle : moins de runtime externe, moins de dépendances système, moins de friction entre build, image et déploiement.
1) Les grands piliers
- Build reproductible avec version Go verrouillée.
- Multi-stage Docker pour séparer compilation et runtime.
- Binaires statiques quand c’est pertinent.
- Images minimales : distroless ou scratch selon le besoin.
- CI/CD strict : fmt, lint, tests, build, scan, release.
- Kubernetes ou autre orchestrateur avec health checks et rollout propre.
- Observabilité : logs, metrics, readiness, graceful shutdown.
2) Chaîne logique de livraison
3) Ce que l’on cherche réellement
| Objectif | Conséquence pratique |
|---|---|
| Image légère | démarrage rapide, surface d’attaque réduite |
| Build déterministe | moins d’écarts entre local, CI et prod |
| Déploiement sûr | rollouts maîtrisés, rollback plus simple |
| Runtime observable | debug plus rapide en incident |
| Hardening correct | meilleure posture sécurité |
4) Diagramme visuel — pipeline Go vers production
5) Règle opérationnelle
- Chaque build doit être traçable.
- Chaque image doit être minimale et scannée.
- Chaque déploiement doit être réversible.
- Chaque service doit avoir des probes et un shutdown propre.
considérer Docker ou Kubernetes comme une “couche ops externe” au code Go. En réalité, le code doit être pensé pour vivre correctement dans ce runtime : timeouts, signaux, health checks, logs structurés et arrêt propre sont des sujets applicatifs autant qu’opérationnels.
Build standard
go build ./cmd/api go build ./... go run ./cmd/api
Build avec métadonnées
go build -ldflags="-X main.version=1.0.0 -X main.commit=abc123 -X main.buildDate=2026-03-19" ./cmd/api
Injecter version, commit et date de build est très utile pour le support, les logs de démarrage, les endpoints de diagnostic et le rollback.
Build reproductible
- Version Go fixée.
go.modetgo.sumpropres.- Build lancé de manière identique en CI et local si besoin.
Cross-compilation
GOOS=linux GOARCH=amd64 go build -o bin/api-linux-amd64 ./cmd/api GOOS=linux GOARCH=arm64 go build -o bin/api-linux-arm64 ./cmd/api GOOS=windows GOARCH=amd64 go build -o bin/api.exe ./cmd/api GOOS=darwin GOARCH=arm64 go build -o bin/api-darwin-arm64 ./cmd/api
Pourquoi Go est fort ici
- Cross-build très simple.
- Très pratique pour agents, CLI, services edge, builds multi-arch.
- Intégration naturelle dans les pipelines release.
Binaires statiques
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o bin/api ./cmd/api
Avec CGO_ENABLED=0, beaucoup d’applications Go peuvent produire un binaire très autonome, particulièrement pratique pour les images minimales, les déploiements simplifiés et les environnements très verrouillés.
Avantages
- Moins de dépendances runtime système.
- Très bon pour scratch/distroless.
- Déploiement simplifié.
- Surface d’attaque réduite.
Attention
- Certains usages de CGO ou dépendances natives empêchent ou compliquent le full static.
- Les besoins TLS/certificats/CA doivent être pensés côté image.
- Il faut valider le comportement réel en container.
Flags fréquents
-ldflags="-s -w"
Ces flags réduisent la taille en supprimant une partie des symboles et informations de debug. À utiliser selon votre politique de debug et vos besoins.
Docker multi-stage : pattern standard
FROM golang:1.25 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -ldflags="-s -w" -o /out/api ./cmd/apiLe premier stage sert à compiler. Il contient toolchain, modules et code source. Le runtime final ne garde que l’exécutable et les éléments strictement nécessaires.
Pourquoi c’est le bon pattern
- Image finale plus petite.
- Moins de surface d’attaque.
- Toolchain absente de l’image runtime.
- Build cache mieux exploitable.
Ordre optimal des couches
- copier
go.modetgo.sum - télécharger les modules
- copier le code
- compiler
Distroless : runtime minimal
FROM gcr.io/distroless/static-debian12 WORKDIR /app COPY --from=builder /out/api /app/api USER nonroot:nonroot EXPOSE 8080 ENTRYPOINT ["/app/api"]
Une image distroless supprime presque tout ce qui n’est pas nécessaire à l’exécution. Cela réduit la taille, la surface d’attaque et la tentation de “bricoler” en prod dans le conteneur.
Distroless vs scratch
| Base | Atout | Attention |
|---|---|---|
| distroless | minimal mais pragmatique | moins d’outils de debug in-container |
| scratch | ultra minimal | encore plus austère, nécessite plus de soin |
Configuration runtime
Une application Go bien déployée lit sa configuration depuis des variables d’environnement ou des fichiers montés, la valide au startup, puis garde un runtime stable et explicite.
type Config struct {
HTTPPort string
DatabaseDSN string
LogLevel string
}Bonnes pratiques
- Valider tôt.
- Ne pas dépendre d’un état mutable caché.
- Différencier secrets et config non sensible.
- Tracer la version applicative au démarrage.
Secrets
- Ne jamais hardcoder dans l’image.
- Utiliser secrets manager, variables injectées ou mounts sécurisés.
- Ne jamais logguer un secret.
- Limiter les privilèges d’accès.
Pipeline CI/CD typique
go version
go mod tidy
git diff --exit-code go.mod go.sum
go fmt ./...
go vet ./...
golangci-lint run
go test ./...
go test -race ./...
go build ./cmd/api
docker build -t myapp:${GIT_SHA} .
trivy image myapp:${GIT_SHA}Étapes utiles
- lint et formatage
- tests unitaires
- race detector si compatible timing pipeline
- build
- scan image
- push registry
- deploy staging puis prod
Qualités d’un bon pipeline
- rapide mais fiable
- reproductible
- échec explicite
- trace le commit et la version
- évite les différences cachées entre branches et environnements
Kubernetes : intégration naturelle
Les services Go s’intègrent très bien à Kubernetes grâce à leur démarrage rapide, leur faible overhead runtime et leur capacité à exposer facilement health checks, metrics et shutdown propre.
livenessProbe:
httpGet:
path: /healthz
port: 8080
readinessProbe:
httpGet:
path: /readyz
port: 8080Ce qu’il faut exposer
/healthzou équivalent/readyzpour indiquer la disponibilité réelle- logs structurés stdout/stderr
- métriques si observabilité avancée
Points clés K8s
- gérer SIGTERM et shutdown propre
- ne pas considérer readiness et liveness comme identiques
- prévoir ressources CPU/mémoire réalistes
- utiliser des images non-root
Rollout et exploitation
- rolling update prudent
- readiness avant prise de trafic
- graceful shutdown avant arrêt complet
- rollback simple si la version dégrade
Ce qu’il faut observer après déploiement
- latence
- erreurs HTTP / métiers
- consommation CPU/mémoire
- timeouts / retries
- nombre de goroutines
Logs et métriques indispensables
- version et commit au démarrage
- erreurs critiques structurées
- health/readiness status
- compteurs de requêtes / jobs / erreurs
- temps de réponse et saturation
Hardening minimal recommandé
- image minimale
- utilisateur non-root
- dépendances scannées
- ports et permissions minimaux
- pas d’outils inutiles dans l’image runtime
USER nonroot:nonroot
Supply chain
- verrouiller versions critiques
- scanner dépendances Go
- scanner images
- générer SBOM si votre chaîne l’exige
Outils utiles
govulncheck ./... trivy image myapp:latest
Éviter
- images builder utilisées en prod
- shells et packages inutiles
- secrets dans l’image
- comptes root par défaut
Code Lab — build multi-stage Go + distroless + shutdown propre
# Dockerfile
FROM golang:1.25 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -ldflags="-s -w -X main.version=1.0.0 -X main.commit=abc123" \
-o /out/api ./cmd/api
FROM gcr.io/distroless/static-debian12
WORKDIR /app
COPY --from=builder /out/api /app/api
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/app/api"]package main
import (
"context"
"errors"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
var (
version = "dev"
commit = "none"
)
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
mux := http.NewServeMux()
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
})
mux.HandleFunc("/readyz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ready"))
})
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}
logger.Info("server starting", "addr", srv.Addr, "version", version, "commit", commit)
go func() {
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
logger.Error("server failed", "err", err)
os.Exit(1)
}
}()
sigCtx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
<-sigCtx.Done()
logger.Info("shutdown signal received")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
logger.Error("shutdown failed", "err", err)
} else {
logger.Info("shutdown complete")
}
}Ce que montre cet exemple
- multi-stage build
- binaire statique
- runtime distroless
- non-root user
- health/readiness endpoints
- graceful shutdown sur signaux
- version injectée au build
Extensions naturelles
- ajouter métriques Prometheus
- ajouter tracing
- ajouter endpoint version
- ajouter Helm chart / manifests K8s
- ajouter pipeline scan + signature
Checklist Build, Docker & Deploy
Build
- version Go fixée
- build reproductible
- ldflags version/commit
- tests/lint avant image
Image
- multi-stage
- image minimale
- non-root
- scan sécurité
Runtime
- health + readiness
- graceful shutdown
- logs structurés
- rollout/rollback maîtrisés
Le déploiement Go moderne repose sur une chaîne sobre et puissante : build propre, binaire simple, Docker multi-stage, runtime minimal, pipeline CI/CD strict, Kubernetes ou plateforme équivalente avec probes et graceful shutdown, puis une observabilité sérieuse. Lorsqu’on relie correctement code, image, pipeline et runtime, Go devient l’un des langages les plus agréables à industrialiser à grande échelle.
En Go, l’accès base de données repose souvent sur
database/sql comme socle standard, éventuellement enrichi par un driver performant, par sqlx pour plus de confort, ou par un ORM comme GORM quand le projet le justifie. La vraie maturité ne consiste pas à choisir “la meilleure lib”, mais à maîtriser les fondamentaux : context, transactions, pool de connexions, erreurs SQL, migrations et observabilité.1) Les piliers d’un accès DB robuste
- database/sql comme abstraction standard.
- driver maîtrisé pour votre SGBD.
- context sur chaque opération réseau/DB.
- transactions quand l’atomicité est nécessaire.
- pool tuning adapté à la charge et au SGBD.
- migrations versionnées et reproductibles.
- métriques et logs pour les requêtes lentes et saturations.
2) Architecture idiomatique
3) Règle d’or
| Sujet | Règle saine |
|---|---|
| context | toujours présent sur requêtes et transactions |
| rows | toujours fermer |
| errors | wrapping + catégorisation |
| pool | toujours réglé, jamais laissé par défaut à l’aveugle |
| migrations | versionnées, testées, répétables |
4) Diagramme visuel — chaîne DB complète
5) But réel
- Requêtes sûres et annulables.
- Charge maîtrisée côté pool.
- Transactions explicites.
- Requêtes observables et optimisables.
considérer la DB comme un simple détail d’implémentation. En réalité, la qualité du code Go backend se voit très vite dans la façon dont il traite les connexions, les timeouts, les transactions et les erreurs SQL.
database/sql : abstraction standard
db, err := sql.Open("postgres", dsn)
if err != nil {
return err
}
defer db.Close()sql.Open ne garantit pas qu’une connexion réelle soit immédiatement établie. Il prépare un handle de pool. Pour vérifier rapidement la connectivité, on utilise généralement PingContext.
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
if err := db.PingContext(ctx); err != nil {
return fmt.Errorf("ping db: %w", err)
}Pourquoi connaître la stdlib même avec ORM
- Comprendre le pool réel.
- Diagnostiquer les saturations et attentes.
- Maîtriser les transactions et le contexte.
- Savoir quoi se passe sous GORM/sqlx.
Objets importants
*sql.DB: handle de pool, pas connexion unique.*sql.Tx: transaction.*sql.Rows: jeu de résultats multi-lignes.*sql.Row: une ligne logique.*sql.Stmt: statement préparé.
*sql.DB : ce n’est pas “la connexion”, c’est le gestionnaire de connexions.Lire une ligne
row := db.QueryRowContext(ctx,
"select id, email from users where id = $1",
id,
)
var u User
if err := row.Scan(&u.ID, &u.Email); err != nil {
return User{}, fmt.Errorf("scan user: %w", err)
}Lire plusieurs lignes
rows, err := db.QueryContext(ctx,
"select id, email from users order by id desc limit 100",
)
if err != nil {
return nil, fmt.Errorf("query users: %w", err)
}
defer rows.Close()Boucle de scan
var users []User
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Email); err != nil {
return nil, fmt.Errorf("scan row: %w", err)
}
users = append(users, u)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("rows error: %w", err)
}Pièges
- Oublier
defer rows.Close(). - Oublier
rows.Err()après la boucle. - Scanner dans un mauvais ordre.
- Faire un
select *fragile si le schéma bouge.
rows non fermées ou mal consommées finissent par dégrader le pool et provoquer des comportements étranges sous charge.Transaction propre
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()_, err = tx.ExecContext(ctx,
"update accounts set balance = balance - $1 where id = $2",
amount, fromID,
)
if err != nil {
return fmt.Errorf("debit source: %w", err)
}
_, err = tx.ExecContext(ctx,
"update accounts set balance = balance + $1 where id = $2",
amount, toID,
)
if err != nil {
return fmt.Errorf("credit target: %w", err)
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("commit transfer: %w", err)
}Règles essentielles
- Toujours
Rollback()en defer. - Utiliser le même
ctxsur toutes les opérations de la transaction. - Garder les transactions courtes.
- Éviter d’y mettre des appels réseau externes si possible.
Ce qu’il faut éviter
- Faire des calculs lents dans la transaction.
- Attendre un service HTTP tiers pendant qu’un verrou DB est tenu.
- Oublier qu’une transaction consomme plus longtemps une connexion du pool.
Réglage du pool
db.SetMaxOpenConns(25) db.SetMaxIdleConns(25) db.SetConnMaxLifetime(30 * time.Minute) db.SetConnMaxIdleTime(5 * time.Minute)
Ce que contrôlent ces paramètres
| Paramètre | Rôle |
|---|---|
| MaxOpenConns | nombre maximal de connexions ouvertes |
| MaxIdleConns | connexions gardées inactives en pool |
| ConnMaxLifetime | durée de vie max d’une connexion |
| ConnMaxIdleTime | durée max d’inactivité avant recyclage |
Pourquoi le tuning est critique
- Trop bas : attente, latence et saturation côté app.
- Trop haut : pression excessive sur le SGBD.
- Durées mal réglées : connexions vieillissantes, effets de LB, timeouts réseau, fuites latentes.
Stats du pool
stats := db.Stats() fmt.Println(stats.OpenConnections) fmt.Println(stats.InUse) fmt.Println(stats.Idle) fmt.Println(stats.WaitCount) fmt.Println(stats.WaitDuration)
sqlx : confort au-dessus de database/sql
sqlx conserve l’esprit SQL explicite tout en réduisant le code de mapping manuel : scan dans structs, requêtes nommées, helpers pratiques. C’est un excellent compromis pour beaucoup de projets Go.
type User struct {
ID int64 `db:"id"`
Email string `db:"email"`
}
var users []User
err := db.SelectContext(ctx, &users,
"select id, email from users where active = true",
)Avantages
- Moins de boilerplate de scan.
- Toujours très proche du SQL réel.
- Bon équilibre entre contrôle et productivité.
Attention
- Il faut toujours comprendre
database/sqldessous. - Le tuning du pool et la gestion du contexte restent inchangés.
- Les erreurs SQL et transactions exigent toujours la même discipline.
sqlx est souvent idéal quand on veut garder la maîtrise SQL sans multiplier le code répétitif de scan et de bind.GORM : ORM plus haut niveau
GORM apporte migrations partielles, modèles, hooks, query builder, associations et productivité rapide pour certains projets. Il peut être pertinent, mais son usage doit rester conscient et observé.
type User struct {
ID uint
Email string
Admin bool
}
var user User
if err := db.WithContext(ctx).First(&user, id).Error; err != nil {
return err
}Quand GORM peut être utile
- Équipe habituée aux ORMs.
- CRUD applicatif rapide.
- Projet interne avec productivité prioritaire.
Quand rester prudent
- Requêtes complexes ou fortement optimisées.
- Besoin de comprendre précisément le SQL émis.
- Tuning très fin et sensibilité forte aux plans d’exécution.
Migrations
Les migrations doivent être versionnées, idempotentes dans leur logique d’exécution attendue, testées en environnement proche du réel et intégrées à la chaîne CI/CD ou au processus de déploiement.
migrations/ 0001_init.sql 0002_add_users.sql 0003_add_index_users_email.sql
Bonnes pratiques
- Une migration = une évolution claire.
- Nommer explicitement les objets créés.
- Séparer schéma et data migrations si besoin.
- Tester les migrations sur copie ou environnement de staging.
Ce qu’il faut surveiller
- Verrous longs sur grosses tables.
- Création d’index coûteuse.
- Ordre de déploiement app / schéma.
- Compatibilité backward/forward lors des déploiements progressifs.
Context sur toutes les opérations
rows, err := db.QueryContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("query users: %w", err)
}Erreurs à catégoriser
- not found logique
- contrainte d’unicité
- timeout / cancellation
- connexion / réseau / auth DB
- erreur interne inattendue
Exemple de wrapping sain
u, err := repo.FindByID(ctx, id)
if err != nil {
return User{}, fmt.Errorf("load user %d: %w", id, err)
}Mapping utile
- Not found → erreur métier claire ou 404 plus haut.
- Contrainte unique → conflit / validation.
- Timeout → métrique de saturation / 504 éventuel.
- DB down → 500 / alerte / dégradation contrôlée.
Perf : où regarder en premier
- temps d’attente du pool
- requêtes lentes
- nombre de lignes lues
- transactions trop longues
- absence ou mauvaise qualité d’index
- surcoût ORM / mapping inutile
stats := db.Stats() _ = stats.WaitCount _ = stats.WaitDuration
Observabilité DB utile
- latence par requête logique
- temps d’attente du pool
- erreurs par type
- compteur de transactions
- requêtes lentes avec seuil
Code Lab — repository SQL propre avec context, tx et pool tuning
package main
import (
"context"
"database/sql"
"fmt"
"time"
)
type User struct {
ID int64
Email string
}
type UserStore struct {
db *sql.DB
}
func NewUserStore(db *sql.DB) UserStore {
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(25)
db.SetConnMaxLifetime(30 * time.Minute)
db.SetConnMaxIdleTime(5 * time.Minute)
return UserStore{db: db}
}
func (s UserStore) FindByID(ctx context.Context, id int64) (User, error) {
row := s.db.QueryRowContext(ctx,
"select id, email from users where id = $1",
id,
)
var u User
if err := row.Scan(&u.ID, &u.Email); err != nil {
return User{}, fmt.Errorf("find user by id=%d: %w", id, err)
}
return u, nil
}
func (s UserStore) TransferCredits(ctx context.Context, fromID, toID int64, amount int64) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("begin transfer tx: %w", err)
}
defer tx.Rollback()
if _, err := tx.ExecContext(ctx,
"update accounts set credits = credits - $1 where id = $2",
amount, fromID,
); err != nil {
return fmt.Errorf("debit account %d: %w", fromID, err)
}
if _, err := tx.ExecContext(ctx,
"update accounts set credits = credits + $1 where id = $2",
amount, toID,
); err != nil {
return fmt.Errorf("credit account %d: %w", toID, err)
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("commit transfer: %w", err)
}
return nil
}
func main() {
// db, _ := sql.Open("postgres", dsn)
// store := NewUserStore(db)
// ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
// defer cancel()
// _, _ = store.FindByID(ctx, 42)
}Ce que montre cet exemple
- pool tuning explicite
- QueryRowContext
- wrapping d’erreurs
- transaction courte et propre
- usage cohérent du context
Améliorations possibles
- ajouter instrumentation metrics/traces
- mapper certaines erreurs SQL en erreurs métier
- ajouter statements préparés ciblés
- intégrer sqlx ou un driver plus spécialisé
Checklist DB Go
Fondamentaux
database/sqlcomprisctxsur chaque requêterows.Close()systématique- erreurs wrapées proprement
Robustesse
- transactions courtes
- pool réglé
- timeouts cohérents
- migrations versionnées
Production
- stats du pool observées
- requêtes lentes instrumentées
- index et plans surveillés
- stratégie de déploiement schéma/app claire
En Go, la maîtrise de la base de données ne se limite pas à savoir écrire une requête. Elle implique de comprendre database/sql, de propager le context, de gérer correctement les transactions, de régler le pool de connexions, de choisir avec discernement entre SQL brut, sqlx ou GORM, et de traiter les migrations et l’observabilité comme des sujets de premier plan. C’est cette discipline globale qui transforme un simple accès DB en couche de persistence réellement fiable et performante.
Go est un langage compilé, typé statiquement, simple en surface mais extrêmement robuste en production, conçu pour construire rapidement des services réseau, des outils systèmes, des backends cloud-native, des pipelines de données et des logiciels d’infrastructure avec une excellente lisibilité, un temps de build court, un runtime fiable et une concurrence de haut niveau via goroutines et channels.
1) Philosophie générale
- Simplicité volontaire : moins de “magie”, moins d’abstractions implicites, syntaxe réduite, onboarding rapide.
- Code lisible avant tout : conventions fortes, formatage automatique, style homogène entre équipes.
- Compilation rapide : feedback loop court, très appréciable en CI/CD et sur gros monorepos.
- Standard library puissante : HTTP, JSON, crypto, context, testing, profiling, templates, compression, net, os, exec.
- Concurrence accessible : goroutines légères, channels, select, context cancellation.
- Exécutable statique : packaging et déploiement souvent plus simples que Java/.NET/Python.
- Pragmatisme : Go privilégie la maintenabilité opérationnelle au raffinement théorique.
2) Carte mentale ultra condensée
3) Où Go excelle particulièrement
| Domaine | Pourquoi Go est fort | Exemples |
|---|---|---|
| API & microservices | Faible overhead, net/http mature, déploiement simple | REST, gRPC, gateways, auth services |
| Cloud & infra | Exécutables portables, concurrence, bon tooling | Kubernetes, Docker, Terraform ecosystem |
| CLI & automation | Binaire unique, démarrage rapide, cross-compilation | DevOps tools, scanners, agents |
| Réseau | TCP/UDP/HTTP solides, I/O efficace | Proxies, load balancers, collectors |
| Observabilité | Instrumentation simple, CPU/memory profiling natif | Metrics exporters, tracers, agents |
4) Diagramme visuel — chaîne complète d’un service Go
5) Go en chiffres “qualitatifs”
- Time-to-first-service : souvent excellent grâce au faible poids conceptuel du langage.
- Coût de maintenance : généralement bas si les équipes respectent idiomes, packages courts et erreurs explicites.
- Scalabilité humaine : très bonne, car les conventions Go limitent les styles “personnels”.
- Scalabilité technique : bonne à très bonne pour I/O, réseau, APIs, background jobs, agents.
Go n’est pas “le meilleur langage pour tout”. Pour du calcul scientifique avancé, du data science interactif, de la métaprogrammation riche, du front web riche, ou des domaines où l’expressivité fonctionnelle poussée est centrale, d’autres langages peuvent être plus adaptés.
Pourquoi les équipes choisissent Go
- Syntaxe compacte et régulière.
- Faible friction entre prototypage et production.
- Très bonne story DevOps : build, test, cross-compile, Docker.
- Runtime mature avec scheduler efficace.
- Écosystème cloud-native massif.
- Excellent pour services backend lisibles par tous.
Pourquoi les ops aiment Go
- Binaire souvent autonome.
- Faible complexité de packaging.
- Démarrage rapide.
- Observation simple avec pprof et expvar/Prometheus.
- Déploiement homogène Linux/containers.
Pourquoi les lead devs aiment Go
- Review facile : moins d’astuces ésotériques.
- Style homogène imposé par fmt.
- Dette technique visible rapidement.
- Faible coût de lecture par les nouveaux arrivants.
Quand ne pas choisir Go en premier choix
| Contexte | Pourquoi | Alternative possible |
|---|---|---|
| Data science exploratoire | Écosystème notebook / ML moins naturel | Python |
| UI front riche | Pas son terrain principal | TypeScript / React |
| Métaprogrammation très avancée | Go reste volontairement simple | Rust / Scala / Lisp family |
| Calcul CPU ultra bas niveau sans GC | Le GC et le modèle runtime peuvent être limitants | Rust / C++ |
Architecture idiomatique d’une application Go
/cmd/api/main.go # entrypoint
/internal/
app/ # bootstrap wiring
http/ # handlers, middleware, routing
service/ # business logic
domain/ # entities, interfaces
store/ # db/repositories
jobs/ # async workers
config/ # typed config
/pkg/ # optional shared libraries
/migrations/
/deploy/
/scripts/
/testdata/Règles saines
- main.go minimal : bootstrap seulement.
- Interfaces là où elles sont consommées, pas partout “par principe”.
- Domain logic découplée du transport HTTP/gRPC.
- Context passé en paramètre pour I/O, timeouts, trace propagation.
- Config typée, validée au startup.
- Observabilité native dès le départ.
Flow d’exécution recommandé
Erreurs à éviter
- Mélanger SQL, logique métier et sérialisation JSON dans le même fichier.
- Créer 200 interfaces inutiles sans bénéfice réel.
- Abuser des packages “utils” fourre-tout.
- Masquer les erreurs réelles sous une couche générique trop tôt.
- Lancer des goroutines sans owner, sans contexte ni stratégie d’arrêt.
Le cœur du langage
Types & simplicité
- Types de base clairs.
- Structs pour la donnée.
- Methods sur types nommés.
- Interfaces satisfaites implicitement.
- Composition > héritage.
Gestion d’erreur explicite
if err != nilpartout où nécessaire.errors.Is,errors.As, wrapping avec%w.- Pas d’exception “globale” comme modèle standard.
Encapsulation
- Export si nom en majuscule.
- Unexported pour internals.
- Packages comme unité d’API.
package domain
import (
"errors"
"fmt"
)
var ErrInvalidEmail = errors.New("invalid email")
type User struct {
ID int64
Name string
Email string
}
func (u User) Validate() error {
if u.Email == "" {
return fmt.Errorf("user validation failed: %w", ErrInvalidEmail)
}
return nil
}Interfaces : l’un des super-pouvoirs de Go
type UserStore interface {
FindByID(ctx context.Context, id int64) (User, error)
Save(ctx context.Context, u User) error
}
type Service struct {
store UserStore
}
func NewService(store UserStore) Service {
return Service{store: store}
}Pourquoi c’est puissant
- Découplage du stockage, du transport, des tests.
- Mocking possible sans framework lourd.
- API lisible si les interfaces sont petites et ciblées.
- Le contrat est comportemental, non hiérarchique.
Concurrence : l’ADN opérationnel de Go
Le modèle de concurrence Go repose sur les goroutines, les channels, le scheduler runtime et le mot-clé select. L’idée n’est pas seulement de “faire du parallèle”, mais d’exprimer des workflows concurrents de manière lisible : fan-out, fan-in, pipelines, workers, cancellation, timeout, backpressure, orchestration de tâches réseau et I/O.
func worker(ctx context.Context, id int, jobs <-chan int, results chan<- int) {
for {
select {
case <-ctx.Done():
return
case job, ok := <-jobs:
if !ok {
return
}
results <- job * 2
}
}
}Points d’attention critiques
- Chaque goroutine doit avoir un owner et une stratégie de fin de vie.
- Channels : définir clairement qui écrit, qui ferme, qui lit.
- Buffering : utile mais ne doit pas masquer un problème de débit.
- Context : obligatoire pour tout ce qui touche I/O, RPC, DB, HTTP.
- Race conditions : exécuter régulièrement
go test -race. - select default : à utiliser avec prudence pour ne pas créer de busy loop.
Pipeline avec timeout, cancellation et collecte d’erreurs
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
g, ctx := errgroup.WithContext(ctx)
for _, item := range items {
item := item
g.Go(func() error {
if err := process(ctx, item); err != nil {
return fmt.Errorf("process item %d: %w", item.ID, err)
}
return nil
})
}
if err := g.Wait(); err != nil {
log.Printf("batch failed: %v", err)
}Standard library & outils à maîtriser absolument
| Catégorie | Packages / outils | Usage |
|---|---|---|
| HTTP | net/http, httptest | Serveurs, clients, tests d’API |
| Context | context | timeouts, cancel, request-scoped values |
| JSON | encoding/json | marshalling/unmarshalling |
| Errors | errors, fmt | wrapping, inspection, propagation |
| Profiling | runtime/pprof, net/http/pprof | CPU/memory/goroutine profiling |
| Tests | testing, benchmark, fuzz | unit, perf, robustness |
| CLI/build | go build, go test, go vet, go fmt | toolchain quotidienne |
# Build
go build ./...
# Run all tests
go test ./...
# Race detector
go test -race ./...
# Benchmarks
go test -bench=. -benchmem ./...
# Vet + formatting
go vet ./...
go fmt ./...
# Module hygiene
go mod tidyCas d’usage #1
API backend. Go est excellent pour des APIs REST/gRPC à fort trafic, avec auth, middlewares, tracing, DB, cache, job queues et déploiements conteneurisés.
Cas d’usage #2
Outils DevOps / SRE. Agents, scanners, probes, outils de migration, orchestrateurs, wrappers CLI, dashboards collectors.
Cas d’usage #3
Plateformes cloud-native. Contrôleurs, opérateurs, sidecars, gateways, API controllers, exporters.
Exemple : mini API HTTP idiomatique
type App struct {
svc Service
log *slog.Logger
}
func (a App) createUser(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var in struct {
Name string `json:"name"`
Email string `json:"email"`
}
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
return
}
u, err := a.svc.CreateUser(ctx, in.Name, in.Email)
if err != nil {
a.log.Error("create user failed", "err", err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(u)
}Performance : comment penser “Go”
- Mesurer avant d’optimiser.
- Regarder allocations, GC pressure, contention, lock hot spots, blocking I/O.
- Comprendre ce qui échappe au stack et part au heap.
- Réduire copies inutiles, allocations transitoires, transformations JSON superflues.
- Pooler avec parcimonie, seulement quand le profiling justifie.
func BenchmarkParser(b *testing.B) {
payload := []byte(`{"name":"alice","email":"a@example.com"}`)
for i := 0; i < b.N; i++ {
var v map[string]any
if err := json.Unmarshal(payload, &v); err != nil {
b.Fatal(err)
}
}
}Toolbox perf
go test -bench=. -benchmem ./...
go test -cpuprofile=cpu.out ./...
go test -memprofile=mem.out ./...
go tool pprof cpu.out
go tool pprof mem.outSécurité applicative & opérationnelle
Transport
- TLS correct, versions et cipher suites maîtrisées.
- Timeouts de serveur configurés.
- Headers de sécurité si exposition web.
Données
- Validation stricte des entrées.
- Pas d’erreurs sensibles exposées au client.
- Secrets hors code source.
Runtime
- Limiter panic non récupérées.
- Shutdown propres.
- Dépendances auditables et minimales.
srv := &http.Server{
Addr: ":8080",
Handler: router,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}| Critère | Go | Python | Java/Kotlin | Rust |
|---|---|---|---|---|
| Courbe d’entrée backend | Très bonne | Excellente | Bonne | Plus raide |
| Déploiement binaire | Excellent | Faible | Moyen | Excellent |
| Concurrence réseau | Excellente | Variable | Excellente | Excellente |
| Expressivité | Moyenne / pragmatique | Élevée | Élevée | Élevée |
| Coût cognitif moyen | Bas | Bas | Moyen | Élevé |
Code lab dense : service HTTP + context + store + graceful shutdown
package main
import (
"context"
"encoding/json"
"errors"
"log"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
type User struct {
ID int64 `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
var ErrInvalidInput = errors.New("invalid input")
type Store interface {
Save(ctx context.Context, u User) (User, error)
}
type MemoryStore struct {
nextID int64
items []User
}
func (m *MemoryStore) Save(ctx context.Context, u User) (User, error) {
select {
case <-ctx.Done():
return User{}, ctx.Err()
default:
}
m.nextID++
u.ID = m.nextID
m.items = append(m.items, u)
return u, nil
}
type Service struct {
store Store
}
func (s Service) CreateUser(ctx context.Context, name, email string) (User, error) {
if name == "" || email == "" {
return User{}, ErrInvalidInput
}
return s.store.Save(ctx, User{Name: name, Email: email})
}
type App struct {
svc Service
log *slog.Logger
}
func (a App) routes() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
})
mux.HandleFunc("/users", a.handleUsers)
return loggingMiddleware(a.log, recoverMiddleware(mux))
}
func (a App) handleUsers(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
var in struct {
Name string `json:"name"`
Email string `json:"email"`
}
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
return
}
u, err := a.svc.CreateUser(ctx, in.Name, in.Email)
if err != nil {
if errors.Is(err, ErrInvalidInput) {
http.Error(w, "invalid input", http.StatusBadRequest)
return
}
a.log.Error("create user failed", "err", err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(u)
}
func loggingMiddleware(logger *slog.Logger, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
logger.Info("http request",
"method", r.Method,
"path", r.URL.Path,
"duration", time.Since(start).String(),
)
})
}
func recoverMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
store := &MemoryStore{}
app := App{
svc: Service{store: store},
log: logger,
}
srv := &http.Server{
Addr: ":8080",
Handler: app.routes(),
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}
go func() {
logger.Info("server starting", "addr", srv.Addr)
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("listen: %v", err)
}
}()
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
<-stop
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
logger.Info("server shutting down")
if err := srv.Shutdown(ctx); err != nil {
logger.Error("shutdown failed", "err", err)
}
}Checklist de maturité Go
Code
- go fmt / go vet intégrés
- packages courts et cohérents
- erreurs wrapées proprement
- interfaces petites
- pas de helpers fourre-tout
Runtime
- timeouts partout sur I/O
- contexts propagés
- shutdown propre
- pas de goroutine leak
- race detector exécuté
Prod
- logs structurés
- metrics + tracing
- health/readiness endpoints
- profiling disponible
- benchmarks sur hot paths
Go brille quand vous voulez des services lisibles, opérables, facilement déployables, performants sans être ésotériques, et capables de gérer proprement la concurrence et le trafic réseau. Son super-pouvoir n’est pas la sophistication syntaxique ; c’est la combinaison rare entre simplicité, discipline, outillage natif et efficacité opérationnelle.
Depuis l’adoption de Go modules, l’installation et la gestion de projets Go sont devenues bien plus simples qu’à l’époque d’un
GOPATH central et omniprésent. Dans un projet moderne, l’essentiel est d’avoir : une version Go cohérente entre développeurs et CI, un go.mod propre, un ensemble d’outils standardisés, et une chaîne de qualité reproductible.1) Objectifs de la toolchain
- Compiler vite et de manière déterministe.
- Tester vite, avec couverture, benchmarks et race detector.
- Formatter automatiquement pour imposer un style commun.
- Résoudre les dépendances proprement via
go.mod/go.sum. - Construire des binaires portables pour Linux, Windows, macOS, containers.
- Déboguer localement avec une expérience simple.
- Protéger la supply chain avec vérification des vulnérabilités et images propres.
2) Principe général d’un setup Go sain
3) Ce qu’il faut absolument standardiser
| Élément | Pourquoi | Recommandation |
|---|---|---|
| Version Go | Éviter les divergences local/CI | Version stable figée par projet |
| Formatage | Uniformité du code | gofmt + goimports |
| Linting | Détecter erreurs et mauvaises pratiques | golangci-lint |
| Build officielle | Reproductibilité | commande documentée unique |
| CI quality gates | Éviter la dérive | fmt, lint, test, vet, build |
4) Diagramme visuel — de l’installation au déploiement
5) Résumé d’architecture de setup
- Machine locale : Go stable + IDE + outils de qualité.
- Projet :
go.mod,go.sum, arborescence claire. - CI : version verrouillée, cache modules, fmt/lint/test/build.
- Container : build multi-stage, image minimale, user non-root.
- Production : variables d’environnement claires, logs structurés, endpoint de santé.
Beaucoup d’équipes installent Go correctement mais négligent le reste : pas de version homogène entre postes, pas de linting standard, pas de règle de build officielle, pas de politique de dépendances. Résultat : les écarts apparaissent rapidement entre local, CI et production.
Matrice d’installation
| Contexte | Approche recommandée | Point d’attention |
|---|---|---|
| Machine de dev Windows | Installer Go stable + vérifier PATH | éviter les multiples versions cachées |
| Machine Linux | Installer version officielle ou package maîtrisé | vérifier cohérence avec la CI |
| macOS | Version stable + extension IDE | attention aux toolchains multiples |
| Runner CI | Version explicitement fixée | cache modules et build |
| Docker builder | Image Go versionnée | ne pas compiler avec une version flottante |
Variables et répertoires importants
- GOROOT : emplacement de l’installation Go. Souvent géré automatiquement.
- GOPATH : espace de travail historique ; toujours utile pour certains bins et caches, mais plus central pour les projets modernes.
- GOMODCACHE : cache des dépendances téléchargées.
- PATH : doit inclure le dossier des exécutables outils installés.
- GOOS / GOARCH : cross-compilation.
Philosophie recommandée
Modules : la base du monde Go moderne
Le système de modules permet de gérer les dépendances sans dépendre d’une arborescence globale de type ancien GOPATH/src/.... Chaque projet possède son fichier go.mod qui décrit l’identité du module, la version Go visée et les dépendances nécessaires.
module github.com/acme/myapp
go 1.25
require (
github.com/go-chi/chi/v5 v5.2.1
github.com/jackc/pgx/v5 v5.7.2
)Rôle des fichiers
- go.mod : identité du module, version Go, dépendances déclarées.
- go.sum : checksums de sécurité et de reproductibilité.
- vendor/ : optionnel selon politique projet ou contraintes d’environnement.
Workspaces et multi-modules
Sur des projets plus gros, ou des monorepos, un go.work peut aider à travailler simultanément sur plusieurs modules locaux sans avoir à publier ou remplacer manuellement chaque dépendance.
go work init ./service-a ./service-b ./shared-lib
go work use ./toolingBonnes pratiques
- Éviter les dépendances inutiles “juste au cas où”.
- Exécuter
go mod tidyrégulièrement. - Réviser les dépendances indirectes lors des mises à jour.
- Ne pas committer un
go.modinstable ou “sale”.
Commandes essentielles
# Vérifier la version installée
go version
# Inspecter l'environnement Go
go env
# Initialiser un nouveau module
go mod init github.com/acme/myapp
# Nettoyer / synchroniser les dépendances
go mod tidy
# Télécharger les dépendances nécessaires
go mod download
# Formater le code
go fmt ./...
# Compiler tous les packages
go build ./...
# Compiler une cible précise
go build ./cmd/api
# Exécuter les tests
go test ./...
# Exécuter les tests avec race detector
go test -race ./...
# Benchmarks
go test -bench=. -benchmem ./...
# Vérification statique standard
go vet ./...
# Lister les dépendances
go list -m all
# Installer un outil
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
# Exécuter un programme
go run ./cmd/apiCommandes de diagnostic très utiles
go env GOMOD
go env GOPATH
go env GOMODCACHE
go list ./...
go test -run TestName ./...
go test -cover ./...
go clean -cache -testcache -modcacheLecture opérationnelle
go envaide énormément quand un poste local “ne se comporte pas comme la CI”.go mod tidydoit faire partie de l’hygiène de routine.go clean -modcacheest précieux si un cache corrompu ou incohérent perturbe le build.go test -raceest indispensable dès que le code concurrent devient sérieux.
Tooling recommandé
| Outil | Rôle | Intérêt concret |
|---|---|---|
| gofmt | formatage standard | style homogène imposé |
| goimports | formatage + imports | nettoie et classe automatiquement |
| golangci-lint | agrégateur de linters | qualité large avec config unique |
| go vet | analyse statique standard | détecte erreurs fréquentes |
| govulncheck | vulnérabilités dépendances | sécurise la supply chain |
| delve | debugger | inspection runtime locale |
| pprof | profiling CPU/mémoire | analyse performance réelle |
Minimum vital
- gofmt
- goimports
- go test
- go vet
Très recommandé
- golangci-lint
- govulncheck
- delve
Pour production sérieuse
- trivy pour images
- pprof
- benchmarks
- coverage
mieux vaut 5 outils systématiquement utilisés que 25 outils “installés quelque part” mais ignorés par l’équipe.
IDE / éditeur
- VS Code + Go extension : très bon compromis, léger, largement adopté.
- GoLand : expérience premium, refactoring et navigation puissants.
- Vim/Neovim : très efficace si l’environnement LSP est bien préparé.
Réglages à activer
- formatage à la sauvegarde
- organisation auto des imports
- exécution rapide des tests
- navigation vers définitions et implémentations
- diagnostics linter visibles dans l’éditeur
Debug avec Delve
# installation
go install github.com/go-delve/delve/cmd/dlv@latest
# debug d'un package
dlv debug ./cmd/api
# test en debug
dlv test ./internal/serviceBuild standard
go build ./cmd/api
# Build avec variables de version
go build -ldflags="-X main.version=1.0.0 -X main.commit=abc123" ./cmd/apiCross-compilation
# Linux AMD64
GOOS=linux GOARCH=amd64 go build -o bin/api-linux-amd64 ./cmd/api
# Linux ARM64
GOOS=linux GOARCH=arm64 go build -o bin/api-linux-arm64 ./cmd/api
# Windows AMD64
GOOS=windows GOARCH=amd64 go build -o bin/api.exe ./cmd/api
# macOS ARM64
GOOS=darwin GOARCH=arm64 go build -o bin/api-darwin-arm64 ./cmd/apiPoints importants
- Documenter la commande de build “officielle”.
- Injecter version, commit, date si utile pour le support.
- Éviter les scripts divergents entre développeurs.
- Vérifier la cohérence des tags de build s’ils existent.
Dockerfile multi-stage recommandé
FROM golang:1.25 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -ldflags="-s -w" -o /out/api ./cmd/api
FROM gcr.io/distroless/static-debian12
WORKDIR /app
COPY --from=builder /out/api /app/api
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/app/api"]Pipeline CI typique
go version
go mod tidy
git diff --exit-code go.mod go.sum
go fmt ./...
go vet ./...
golangci-lint run
go test ./...
go test -race ./...
go build ./cmd/apiBonnes pratiques CI
- Version Go explicite et unique.
- Cache des modules et du build si pertinent.
- Échec immédiat sur formatage/lint/tests.
- Artifact final versionné.
- Scan image container si build Docker.
Sécurité de la toolchain
Dépendances
- go.sum versionné
- govulncheck
- revue des upgrades
- attention aux dépendances indirectes
Build / containers
- image minimale
- utilisateur non-root
- scan Trivy
- surface d’attaque réduite
Poste local
- éviter scripts obscurs
- maîtriser PATH
- éviter versions flottantes
- documenter les outils autorisés
# Vérification vulnérabilités Go
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...
# Scan image container
trivy image myapp:latestTroubleshooting — problèmes classiques
| Symptôme | Cause possible | Piste de résolution |
|---|---|---|
| La CI ne compile pas comme en local | version Go différente | aligner local/CI, vérifier go version |
| Dépendances incohérentes | go.mod / go.sum sales | go mod tidy, revue commit |
| Outil non trouvé | PATH incomplet | vérifier emplacement des bins installés |
| Build Docker lent | cache modules mal exploité | copier go.mod/go.sum avant le code |
| Tests intermittents | race, ordre, timing | go test -race, isoler états partagés |
| Module cache corrompu | cache local problématique | go clean -modcache |
Commande diagnostic “réflexe”
go version
go env
go env GOMOD
go env GOPATH
go env GOMODCACHE
go mod tidy
go test ./...
go test -race ./...Méthode recommandée
- Comparer version Go locale et CI.
- Inspecter l’environnement réel avec
go env. - Nettoyer dépendances et cache si nécessaire.
- Repasser fmt/lint/test/build dans le même ordre que la CI.
- Ne pas patcher au hasard sans comprendre la divergence.
Code Lab — bootstrap de projet Go propre
# 1) créer le dossier
mkdir myapp
cd myapp
# 2) initialiser le module
go mod init github.com/acme/myapp
# 3) structure minimale
mkdir -p cmd/api internal/service internal/http
# 4) main.go
cat > cmd/api/main.go <<'EOF'
package main
import (
"log"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
})
log.Println("listening on :8080")
if err := http.ListenAndServe(":8080", mux); err != nil {
log.Fatal(err)
}
}
EOF
# 5) premier run
go run ./cmd/api
# 6) formatage
go fmt ./...
# 7) test futur / build
go build ./cmd/apiCe que ce bootstrap vous donne
- un module propre
- une entrée explicite dans
cmd/api - une base prête pour handlers, services, stores
- une commande de build claire
Évolution naturelle
- ajout d’un logger structuré
- config par variables d’environnement
- tests unitaires
- Dockerfile multi-stage
- linting et CI
Checklist de setup professionnel
Installation
- Installer Go stable
- Vérifier
go version - Contrôler
go env - Vérifier PATH/outils
Projet
- Créer
go.mod - Définir la commande build officielle
- Activer formatage auto
- Ajouter tests et lint
Industrialisation
- Version Go identique en CI
- Pipeline fmt/vet/lint/test/build
- Image Docker minimale
- Scan sécurité + doc onboarding
Un bon setup Go ne se résume pas à “installer le compilateur”. Il faut penser la chaîne complète : installation, versionnage, modules, formatage, linting, tests, build, containerisation, CI, sécurité. C’est cette cohérence d’ensemble qui donne à Go sa fameuse efficacité en production.
Go privilégie la déclaration explicite, les blocs courts, les conventions fortes et une syntaxe volontairement réduite. Le langage cherche moins à “impressionner” qu’à produire un code homogène, lisible et facile à relire dans six mois par une autre équipe. Beaucoup de patterns idiomatiques — en particulier les blocs
if err != nil — peuvent sembler répétitifs au début, mais ils rendent le flux de contrôle clair, visible et très maintenable.1) Les grands principes de syntaxe Go
- Peu de mots-clés, peu de magie. Le langage reste compact.
- Bloc avec accolades obligatoires. Pas de style “one-liner ambigu”.
- Pas de parenthèses autour des conditions dans
if,for,switch. - Point-virgule implicite dans la plupart des cas.
- Déclarations courtes avec
:=très utilisées à l’intérieur des fonctions. - Visibilité via la casse : nom commençant par une majuscule = exporté.
- Pas d’héritage classique ; composition, interfaces et methods dominent.
2) Lecture rapide d’un programme Go minimal
package main
import "fmt"
func main() {
fmt.Println("hello, Go")
}package main: définit le package d’entrée d’un exécutable.import "fmt": importe le package standard de formatage.func main(): point d’entrée du programme.
3) Vue d’ensemble de la syntaxe
4) Ce qui surprend souvent au début
| Point | En Go | Conséquence pratique |
|---|---|---|
| Pas de classes | structs + methods + interfaces | modèle plus simple, composition forte |
| Pas d’exceptions classiques | erreurs explicites | flux d’erreur visible dans le code |
| Une seule boucle | for | syntaxe uniforme pour itération |
| Visibilité par casse | Majuscule = public | API de package simple à comprendre |
| Formatage imposé | gofmt | moins de débats de style |
5) Diagramme visuel — structure syntaxique d’un fichier Go
6) Réflexe syntaxique idiomatique
vouloir “écrire du Go comme un autre langage”. Les développeurs qui transposent trop directement des réflexes Java, Python ou C++ finissent souvent avec un code Go plus lourd, moins idiomatique et paradoxalement plus difficile à maintenir.
Variables, constantes et inférence
package main
import "fmt"
func main() {
var name string = "Guillaume"
age := 42
const pi = 3.14159
fmt.Println(name, age, pi)
}Modes de déclaration
var x int: déclaration explicite avec valeur zéro.var x = 10: type inféré.x := 10: déclaration courte, très idiomatique dans une fonction.const: valeur constante connue à la compilation ou utilisable comme constante.
Valeurs zéro importantes
| Type | Valeur zéro |
|---|---|
| int | 0 |
| string | "" |
| bool | false |
| pointer | nil |
| slice | nil |
| map | nil |
| interface | nil |
Types numériques, chaînes, booléens
var (
a int = 10
b int64 = 20
c float64 = 3.14
ok bool = true
s string = "hello"
)Conversions explicites
var a int = 10
var b int64 = int64(a)
var c float64 = float64(a)- Go évite les conversions implicites trop permissives.
- Cette discipline réduit les ambiguïtés et rend le code plus sûr.
:= est très pratique, mais ne doit pas rendre le code flou. Dans les scopes importants ou sur des valeurs métier critiques, un type explicite peut améliorer la lisibilité.if, for, switch : le trio central
if x > 10 {
fmt.Println("big")
} else if x == 10 {
fmt.Println("equal")
} else {
fmt.Println("small")
}for i := 0; i < 5; i++ {
fmt.Println(i)
}switch day {
case "mon":
fmt.Println("start")
case "fri":
fmt.Println("end")
default:
fmt.Println("mid")
}Particularités importantes
- Pas de parenthèses autour des conditions.
forremplace aussiwhile.switchest puissant, lisible et très utilisé.- Un
switchGo n’a pas besoin debreakdans chaque case.
Exemples utiles
for x < 100 {
x *= 2
}for _, item := range items {
fmt.Println(item)
}switch {
case err != nil:
fmt.Println("error")
case count == 0:
fmt.Println("empty")
default:
fmt.Println("ok")
}range est pratique, mais il faut bien comprendre ce que l’on récupère : index, valeur, ou les deux. Dans certains cas avancés, notamment avec des pointeurs ou des closures, une mauvaise compréhension du scope de variable peut provoquer des bugs subtils.Fonctions simples
func add(a, b int) int {
return a + b
}func split(sum int) (int, int) {
x := sum * 4 / 9
y := sum - x
return x, y
}Spécificités notables
- Types à droite des paramètres.
- Paramètres adjacents de même type groupables.
- Retours multiples très fréquents.
- Les retours multiples sont très utilisés pour résultat + erreur.
Fonctions anonymes et closures
func main() {
double := func(n int) int {
return n * 2
}
fmt.Println(double(21))
}func makeCounter() func() int {
n := 0
return func() int {
n++
return n
}
}Nommer les retours : avec modération
func stats() (count int, ok bool) {
count = 42
ok = true
return
}Structs : la base des modèles de données
type User struct {
ID int64
Name string
Email string
}u := User{
ID: 1,
Name: "Alice",
Email: "alice@example.com",
}Pourquoi les structs sont centrales
- Elles remplacent souvent l’idée intuitive de “classe” pour la donnée.
- Elles peuvent embarquer des tags utiles, par exemple JSON, DB, validation.
- Elles se combinent très bien avec methods et interfaces.
Methods sur types
type User struct {
Name string
}
func (u User) Display() string {
return "User: " + u.Name
}type Counter struct {
Value int
}
func (c *Counter) Inc() {
c.Value++
}Value receiver vs pointer receiver
| Type de receiver | Usage principal |
|---|---|
| receiver par valeur | lecture, copie acceptable, type léger |
| receiver par pointeur | mutation, éviter copie coûteuse |
Interfaces : contrat comportemental
type Reader interface {
Read(p []byte) (n int, err error)
}Une interface décrit un comportement. En Go, un type satisfait une interface implicitement s’il possède les methods requises. Il n’a pas besoin de déclaration formelle “implements”.
type Greeter interface {
Greet() string
}
type Person struct {
Name string
}
func (p Person) Greet() string {
return "Hello " + p.Name
}Pourquoi c’est puissant
- Découplage très propre entre implémentation et usage.
- Testabilité améliorée.
- Moins de hiérarchie lourde qu’en programmation orientée objet classique.
- Conduit naturellement à des interfaces petites et ciblées.
Le pattern fondamental : résultat + erreur
func parse(input string) (int, error) {
if input == "" {
return 0, errors.New("empty input")
}
return strconv.Atoi(input)
}n, err := parse("42")
if err != nil {
return err
}
fmt.Println(n)Bloc idiomatique central
if err != nil {
return err
}- Les erreurs ne sont pas “cachées”.
- Le flux d’échec reste visible et proche de l’appel fautif.
- La lecture du comportement devient très directe.
Wrapping et inspection
if err != nil {
return fmt.Errorf("load config: %w", err)
}if errors.Is(err, os.ErrNotExist) {
fmt.Println("file missing")
}var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Println(pathErr.Path)
}defer : fermeture et nettoyage fiables
defer exécute une instruction à la sortie de la fonction courante, quel que soit le chemin normal de sortie. C’est un outil essentiel pour fermer un fichier, relâcher une ressource, débloquer un mutex ou tracer une fin de fonction.
f, err := os.Open("data.txt")
if err != nil {
return err
}
defer f.Close()mu.Lock()
defer mu.Unlock()panic et recover
func safe() {
defer func() {
if r := recover(); r != nil {
fmt.Println("recovered:", r)
}
}()
panic("boom")
}- panic n’est pas le mécanisme normal de gestion d’erreur.
- recover doit être réservé à certains cadres bien maîtrisés, comme un middleware serveur ou un boundary critique.
- Pour les erreurs métier et I/O, on retourne généralement
error.
panic pour des erreurs applicatives ordinaires est presque toujours une mauvaise idée. Cela dégrade la lisibilité, le contrôle du flux et la robustesse opérationnelle.Pointeurs en Go
Les pointeurs existent, mais sans arithmétique de pointeurs classique comme en C. Ils servent principalement à éviter certaines copies, à partager un état mutable, ou à distinguer “absence de valeur” et “valeur zéro” dans certaines API.
x := 10
p := &x
fmt.Println(*p) // 10
*p = 20
fmt.Println(x) // 20Cas d’usage typiques
- Receivers de methods qui mutent la struct.
- Types lourds à copier.
- Structures optionnelles ou champs “nullable-like”.
À bien comprendre
- Un pointeur peut être
nil. - Go gère la mémoire automatiquement ; on ne fait pas de
free()manuel. - Le choix valeur/pointeur influence les methods satisfaites par une interface.
type Config struct {
Port int
}
func update(c *Config) {
c.Port = 9090
}Arrays, slices et maps
arr := [3]int{1, 2, 3}
slice := []int{10, 20, 30}
m := map[string]int{
"alice": 1,
"bob": 2,
}Slices : structure fondamentale
- Très utilisées pour les collections ordonnées dynamiques.
- Basées sur un tableau sous-jacent.
- Manipulées avec
append,len,cap.
nums := []int{1, 2}
nums = append(nums, 3, 4)Maps : dictionnaires
ages := map[string]int{}
ages["alice"] = 30
age, ok := ages["alice"]
if ok {
fmt.Println(age)
}Range sur collections
for i, v := range slice {
fmt.Println(i, v)
}
for k, v := range m {
fmt.Println(k, v)
}nil. La lecture est possible, mais l’écriture provoquera une panic. Il faut donc l’initialiser avant usage si l’on veut écrire dedans.Code Lab — mini programme dense et idiomatique
package main
import (
"errors"
"fmt"
"strings"
)
type User struct {
Name string
Email string
}
func (u User) Validate() error {
if strings.TrimSpace(u.Name) == "" {
return errors.New("name is required")
}
if !strings.Contains(u.Email, "@") {
return errors.New("invalid email")
}
return nil
}
func NewUser(name, email string) (User, error) {
u := User{
Name: name,
Email: email,
}
if err := u.Validate(); err != nil {
return User{}, fmt.Errorf("new user validation failed: %w", err)
}
return u, nil
}
func main() {
users := []User{
{Name: "Alice", Email: "alice@example.com"},
{Name: "Bob", Email: "bob@example.com"},
}
for _, u := range users {
fmt.Println(u.Name, u.Email)
}
created, err := NewUser("Guillaume", "guillaume@example.com")
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println("created:", created)
}Ce que montre cet exemple
- struct simple
- method de validation
- retour résultat + erreur
- wrapping d’erreur
- slice + range
Comment l’étendre
- ajouter JSON tags
- ajouter persistence DB
- ajouter HTTP handler
- ajouter tests unitaires
- ajouter context si I/O
Checklist de syntaxe idiomatique
Lecture du code
- package clair
- imports propres
- noms simples et cohérents
- fonctions courtes
Flux de contrôle
if err != nilproche de l’appel- switch privilégié quand utile
- range bien compris
- defer pour les ressources
Modélisation
- structs simples
- methods pertinentes
- interfaces petites
- pointeurs seulement quand utiles
La syntaxe Go n’est pas minimaliste par hasard : elle est conçue pour rendre visibles les structures du programme, les points d’échec, les mutations et les dépendances. Plus on adopte les idiomes natifs — code clair, fonctions courtes, erreurs explicites, structs simples, interfaces ciblées — plus le langage révèle sa véritable force : une lisibilité opérationnelle exceptionnelle.
Là où d’autres langages mettent les classes au centre, Go place la struct comme conteneur de données, puis attache du comportement via des methods. Cette séparation simple entre donnée et comportement rend le code plus explicite, plus lisible et souvent plus facile à faire évoluer.
1) Définition de base
type User struct {
ID int64
Email string
Admin bool
}
func (u User) IsAdmin() bool {
return u.Admin
}2) Pourquoi les structs sont si importantes
- Elles modélisent l’état d’un objet métier sans machinery lourde.
- Elles sont très simples à instancier, copier, sérialiser, tester.
- Elles fonctionnent naturellement avec JSON, DB, validation et méthodes.
- Elles encouragent une architecture “data + behavior” claire, sans hiérarchie complexe.
3) Instanciation idiomatique
u := User{
ID: 1,
Email: "alice@example.com",
Admin: true,
}var u User
u.Email = "bob@example.com"4) Tags fréquents
type User struct {
ID int64 `json:"id" db:"id"`
Email string `json:"email" db:"email"`
Admin bool `json:"admin" db:"admin"`
}Les tags de struct servent à transmettre des métadonnées à certains packages ou frameworks : JSON, ORM, validation, YAML, etc. Ils ne changent pas le langage lui-même, mais enrichissent la façon dont la struct est exploitée par l’écosystème.
5) Diagramme conceptuel
6) Ce qu’une struct n’est pas
- Ce n’est pas une “classe Java déguisée”.
- Ce n’est pas forcément un objet riche avec des dizaines de methods.
- Ce n’est pas un prétexte pour regrouper trop d’état hétérogène.
créer des structs énormes qui mélangent données métier, état technique, dépendances externes, configuration et logique de transport. En Go, une struct saine reste généralement ciblée et cohérente.
Methods : attacher du comportement à un type
type User struct {
ID int64
Email string
Admin bool
}
func (u User) IsAdmin() bool {
return u.Admin
}Receiver par valeur
func (u User) Domain() string {
parts := strings.Split(u.Email, "@")
if len(parts) != 2 {
return ""
}
return parts[1]
}Un receiver par valeur copie la valeur du receiver. C’est très bien pour des types petits, des méthodes purement lectrices, ou quand la sémantique de copie est acceptable.
Receiver par pointeur
type Counter struct {
Value int
}
func (c *Counter) Inc() {
c.Value++
}Le receiver par pointeur est à privilégier lorsqu’une méthode modifie la struct, ou lorsqu’on veut éviter une copie coûteuse d’un type volumineux.
Choisir valeur ou pointeur
| Situation | Receiver conseillé |
|---|---|
| Type petit, lecture seule | valeur |
| Mutation d’état | pointeur |
| Struct volumineuse | pointeur |
| API cohérente sur un même type | souvent uniforme |
Important : méthode set et interface
type Renamer interface {
Rename(string)
}
type User struct {
Name string
}
func (u *User) Rename(v string) {
u.Name = v
}Ici, c’est *User qui satisfait l’interface, pas User. Le choix du receiver influence donc la satisfaction des interfaces et doit être bien compris.
Interfaces : contrat comportemental minimal
Les interfaces sont satisfaites implicitement. C’est une force énorme pour le découplage, les tests et la modularité. Un type n’a pas besoin de déclarer explicitement qu’il “implémente” une interface ; il lui suffit de fournir les methods attendues.
type Mailer interface {
Send(to, body string) error
}type SMTPMailer struct{}
func (m SMTPMailer) Send(to, body string) error {
fmt.Println("sending mail to", to)
return nil
}func NotifyWelcome(m Mailer, email string) error {
return m.Send(email, "welcome")
}Pourquoi ce modèle est si puissant
- Il découple le code consommateur de l’implémentation concrète.
- Il favorise les tests sans framework lourd.
- Il encourage des contrats petits, lisibles et ciblés.
- Il évite les hiérarchies de types excessives.
Petites interfaces > grosses interfaces
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}Les petites interfaces composables sont un trait fort de Go. Une interface trop grosse devient rigide, difficile à satisfaire, difficile à tester et révèle souvent un design flou.
Composition plutôt qu’héritage
Go préfère la composition à l’héritage. Au lieu de construire des arbres de classes, on assemble des types plus simples et spécialisés. Cela réduit le couplage, rend les relations plus explicites et simplifie les évolutions.
type Address struct {
City string
Country string
}
type User struct {
ID int64
Email string
Address Address
}Ici, User compose une Address. Il n’y a pas de sous-classe ni d’arbre d’héritage ; juste un type qui en contient un autre.
Pourquoi la composition est saine
- Les dépendances structurelles restent visibles.
- Les effets de bord liés à l’héritage profond sont évités.
- Le code devient plus modulaire et plus testable.
- Le refactoring est souvent plus simple.
Embedding : composition avec promotion de champs/methods
L’embedding permet d’inclure un type dans une struct sans lui donner explicitement un nom de champ. Les fields et methods du type embarqué deviennent alors “promus” sur la struct englobante, ce qui facilite certaines formes de réutilisation.
type Audit struct {
CreatedAt time.Time
UpdatedAt time.Time
}
type User struct {
ID int64
Email string
Audit
}u := User{}
u.CreatedAt = time.Now()Ce que l’embedding n’est pas
- Ce n’est pas un héritage classique.
- Ce n’est pas une hiérarchie polymorphique lourde.
- Ce n’est pas un prétexte pour cacher trop d’état partagé.
Bon usage
- Mutualiser des champs transverses : audit, timestamps, metadata.
- Réutiliser un petit comportement commun.
- Conserver une API lisible.
Patterns de design idiomatiques
Entity + Service
type User struct {
ID int64
Email string
}
type Service struct {
store UserStore
}Sépare données métier et orchestration.
Repository interface
type UserStore interface {
FindByID(ctx context.Context, id int64) (User, error)
Save(ctx context.Context, u User) error
}Découple domaine et persistence.
Ports & adapters léger
type Mailer interface {
Send(to, body string) error
}Le domaine dépend d’un contrat, pas d’une implémentation.
Exemple de service orienté interface
type UserStore interface {
Save(ctx context.Context, u User) error
}
type Service struct {
store UserStore
}
func NewService(store UserStore) Service {
return Service{store: store}
}
func (s Service) Register(ctx context.Context, u User) error {
if u.Email == "" {
return errors.New("email required")
}
return s.store.Save(ctx, u)
}Pourquoi ce pattern fonctionne bien en Go
- Il garde le domaine lisible.
- Il permet des tests faciles.
- Il ne nécessite aucun framework invasif.
- Il reste compatible avec une architecture simple ou plus ambitieuse.
Tester via petites interfaces
type Mailer interface {
Send(to, body string) error
}
type FakeMailer struct {
Calls []string
}
func (f *FakeMailer) Send(to, body string) error {
f.Calls = append(f.Calls, to+"|"+body)
return nil
}func TestWelcome(t *testing.T) {
fake := &FakeMailer{}
err := NotifyWelcome(fake, "alice@example.com")
if err != nil {
t.Fatal(err)
}
if len(fake.Calls) != 1 {
t.Fatalf("expected 1 call, got %d", len(fake.Calls))
}
}Pourquoi Go simplifie le mocking
- Pas besoin de framework complexe pour de nombreux cas.
- Une petite interface suffit.
- Les fakes en mémoire sont souvent plus lisibles qu’un mock DSL sophistiqué.
- Les tests deviennent plus proches du comportement métier réel.
Anti-patterns fréquents
| Anti-pattern | Pourquoi c’est mauvais | Alternative saine |
|---|---|---|
| God struct | trop d’état, trop de responsabilités | diviser en types cohérents |
| Interface géante | rigide, difficile à mocker | plusieurs petites interfaces |
| Interface prématurée | abstraction inutile | attendre un vrai besoin de découplage |
| Embedding excessif | origine des fields floue | composition explicite si besoin |
| Receiver incohérents | API confuse | stratégie uniforme par type |
Zero values utiles
Une bonne pratique Go consiste, quand c’est possible, à rendre les zero values immédiatement utilisables. Cela réduit le besoin d’initialisations complexes et simplifie l’usage des types.
type Counter struct {
Value int
}
func (c *Counter) Inc() {
c.Value++
}Ici, la zero value de Counter est déjà valide : var c Counter peut être utilisé directement.
Pourquoi c’est précieux
- API plus simples.
- Moins de constructeurs obligatoires.
- Moins de risques d’oublier une initialisation.
- Meilleure ergonomie générale.
var c Counter
c.Inc()
fmt.Println(c.Value) // 1Architecture applicative typique
Dans une application Go propre, les structs modélisent les entités et objets utiles, les services orchestrent la logique, et les interfaces marquent les frontières avec les dépendances externes.
Répartition saine des responsabilités
- Struct métier : état cohérent + methods proches du métier.
- Service : orchestration, validation, workflow.
- Interface : frontière entre domaine et infrastructure.
- Implémentation concrète : DB, SMTP, Redis, HTTP client, etc.
Code Lab — struct + method + interface + composition
package main
import (
"context"
"errors"
"fmt"
"strings"
)
type User struct {
ID int64
Email string
Admin bool
}
func (u User) IsAdmin() bool {
return u.Admin
}
func (u User) Validate() error {
if !strings.Contains(u.Email, "@") {
return errors.New("invalid email")
}
return nil
}
type Mailer interface {
Send(to, body string) error
}
type ConsoleMailer struct{}
func (m ConsoleMailer) Send(to, body string) error {
fmt.Println("send mail to:", to, "body:", body)
return nil
}
type UserStore interface {
Save(ctx context.Context, u User) error
}
type MemoryStore struct {
Users []User
}
func (m *MemoryStore) Save(ctx context.Context, u User) error {
m.Users = append(m.Users, u)
return nil
}
type Service struct {
store UserStore
mailer Mailer
}
func NewService(store UserStore, mailer Mailer) Service {
return Service{
store: store,
mailer: mailer,
}
}
func (s Service) Register(ctx context.Context, u User) error {
if err := u.Validate(); err != nil {
return fmt.Errorf("validate user: %w", err)
}
if err := s.store.Save(ctx, u); err != nil {
return fmt.Errorf("save user: %w", err)
}
if err := s.mailer.Send(u.Email, "welcome"); err != nil {
return fmt.Errorf("send welcome mail: %w", err)
}
return nil
}
func main() {
store := &MemoryStore{}
mailer := ConsoleMailer{}
svc := NewService(store, mailer)
u := User{
ID: 1,
Email: "alice@example.com",
Admin: true,
}
if err := svc.Register(context.Background(), u); err != nil {
fmt.Println("error:", err)
return
}
fmt.Println("registered users:", len(store.Users))
fmt.Println("is admin:", u.IsAdmin())
}Ce que montre cet exemple
- struct métier simple
- methods de validation et lecture
- interfaces petites et ciblées
- service injecté par dépendances
- implémentations concrètes interchangeables
Extensions possibles
- ajouter une implémentation Postgres
- ajouter un fake mailer pour tests
- ajouter des JSON tags
- ajouter du context timeout
- ajouter métriques et logs
Best practices
Structs
- Éviter les “god structs”.
- Regrouper seulement des champs cohérents.
- Rendre la zero-value utile quand possible.
- Utiliser des noms simples et métier.
Interfaces
- Définir les interfaces au point d’usage, pas trop haut dans la stack.
- Favoriser de petites interfaces ciblées.
- Ne pas abstraire avant d’en avoir besoin.
- Mocker avec de petits fakes lisibles.
Methods & design
- Rester cohérent sur valeur vs pointeur.
- Utiliser la composition avant toute hiérarchie artificielle.
- Préférer l’explicite à la magie.
- Garder les frontières domaine / infra lisibles.
La combinaison structs + methods + interfaces + composition constitue l’un des noyaux les plus puissants de Go. Les structs portent l’état, les methods portent le comportement, les interfaces définissent des contrats légers, et la composition remplace élégamment l’héritage lourd. Bien utilisée, cette approche produit un code extrêmement lisible, testable et robuste, particulièrement adapté aux services backend, aux outils d’infrastructure et aux architectures modulaires.
En Go, l’erreur n’est pas un événement “exceptionnel” caché dans un mécanisme global d’exceptions. C’est une valeur explicite, retournée, transmise, enrichie, inspectée et traitée là où cela a du sens. Cette approche produit un code plus verbeux, oui, mais aussi beaucoup plus lisible en production : on voit où une opération peut échouer, on voit comment l’échec remonte, et on voit qui décide de la réponse finale.
1) Pattern fondamental
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("read config %s: %w", path, err)
}2) Pourquoi ce pattern est si central
- L’échec reste visible dans le flux normal du programme.
- Le contexte métier ou technique peut être ajouté immédiatement.
- On évite les exceptions qui remontent “hors champ” sans signal explicite.
- Le code de lecture et d’exploitation des erreurs reste simple et local.
3) Lecture mentale idiomatique
4) Exemple progressif
func loadConfig(path string) ([]byte, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read config %s: %w", path, err)
}
return data, nil
}Le retour multiple (value, error) est l’un des idiomes majeurs du langage. Une fonction qui peut échouer doit le dire dans sa signature.
5) Diagramme — chaîne de propagation d’erreur
6) Règle mentale utile
- La couche basse détecte l’échec.
- La couche intermédiaire ajoute du contexte.
- La frontière décide de la réponse externe et du logging.
soit ignorer l’erreur, soit la “traiter” trop tôt en la loggant partout, soit la transformer en simple chaîne sans possibilité d’inspection. Une erreur Go doit rester exploitable.
Wrapping avec %w
Utiliser %w permet de conserver la chaîne causale et d’exploiter ensuite errors.Is et errors.As. On enrichit donc l’erreur sans perdre sa nature profonde.
func load(path string) ([]byte, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("load file %s: %w", path, err)
}
return data, nil
}Pourquoi c’est supérieur à une simple concaténation
- Le message devient plus utile pour l’humain.
- La cause sous-jacente reste inspectable.
- Les tests peuvent continuer à reconnaître certains cas particuliers.
- Les couches supérieures peuvent encore prendre des décisions intelligentes.
Bon wrapping vs mauvais wrapping
// bon
return fmt.Errorf("read config %s: %w", path, err)
// moins bon : perd la causalité structurée
return fmt.Errorf("read config %s: %v", path, err)
// encore pire : transforme en simple texte
return errors.New("read config failed")Règles pratiques
- Ajouter un contexte utile, pas décoratif.
- Ne pas re-wrapper 7 fois sans information nouvelle.
- Conserver l’erreur originale quand la couche supérieure doit l’identifier.
- Choisir un message orienté action ou diagnostic.
errors.Is : reconnaître un cas
if errors.Is(err, os.ErrNotExist) {
fmt.Println("file not found")
}errors.Is permet de dire : “dans cette chaîne d’erreurs wrapées, y a-t-il cette erreur cible ?”. C’est idéal pour les erreurs sentinelles ou certains cas standards du runtime / OS / I/O.
errors.As : extraire un type d’erreur
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Println("path:", pathErr.Path)
}errors.As sert lorsque l’on veut récupérer un type précis d’erreur pour accéder à ses champs ou à son comportement.
Quand utiliser l’un ou l’autre
| Besoin | Outil |
|---|---|
| Tester une erreur connue | errors.Is |
| Récupérer un type d’erreur structuré | errors.As |
Comparer naïvement avec == | à éviter sur erreurs wrapées |
Exemple réaliste
func readUserConfig(path string) error {
_, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("user config loading failed: %w", err)
}
return nil
}
err := readUserConfig("/tmp/missing.json")
if errors.Is(err, os.ErrNotExist) {
fmt.Println("create a default config")
}Sentinel errors
var ErrInvalidEmail = errors.New("invalid email")
var ErrUserNotFound = errors.New("user not found")Une sentinel error représente un cas connu et identifiable. Elle est simple, lisible, mais ne transporte pas de métadonnées détaillées.
Custom error type
type ValidationError struct {
Field string
Msg string
}
func (e ValidationError) Error() string {
return e.Field + ": " + e.Msg
}Un type personnalisé est utile lorsque l’erreur doit transporter des informations structurées : champ en cause, code, ressource, statut, catégorie, etc.
Quand choisir quoi
| Situation | Choix adapté |
|---|---|
| Cas binaire simple et fréquent | sentinel error |
| Besoin de métadonnées | custom type |
| Besoin d’API stable cross-layer | souvent sentinel + wrapping |
| Validation riche | custom type ou agrégat |
Exemple combiné
var ErrUnauthorized = errors.New("unauthorized")
type HTTPError struct {
Status int
Op string
Err error
}
func (e HTTPError) Error() string {
return fmt.Sprintf("%s: %v", e.Op, e.Err)
}
func (e HTTPError) Unwrap() error {
return e.Err
}Erreurs liées au context
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
err := doWork(ctx)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
fmt.Println("timeout")
}
}Les timeouts et annulations sont essentiels dans les applications I/O, HTTP, DB, gRPC et traitement asynchrone. Le context fournit des erreurs standards que vos couches supérieures doivent souvent reconnaître.
I/O : toujours contextualiser
rows, err := db.QueryContext(ctx, query)
if err != nil {
return fmt.Errorf("query users: %w", err)
}Pourquoi c’est critique en production
- Un timeout n’a pas la même signification qu’une erreur métier.
- Une annulation volontaire ne doit pas toujours déclencher une alerte.
- Les retries automatiques doivent distinguer timeout, 4xx, 5xx, réseau, validation.
- Le diagnostic opérationnel dépend fortement de cette catégorisation.
Service layer : enrichir, pas exposer
func (s Service) GetUser(ctx context.Context, id int64) (User, error) {
u, err := s.store.FindByID(ctx, id)
if err != nil {
return User{}, fmt.Errorf("get user %d: %w", id, err)
}
return u, nil
}La couche service donne du sens fonctionnel ou métier à l’erreur. Elle ne doit pas forcément décider du code HTTP ou du message client final, mais elle doit rendre l’échec intelligible.
HTTP handler : mapper vers une réponse sûre
u, err := svc.GetUser(r.Context(), id)
if err != nil {
switch {
case errors.Is(err, ErrUserNotFound):
http.Error(w, "user not found", http.StatusNotFound)
default:
http.Error(w, "internal error", http.StatusInternalServerError)
}
return
}Règle essentielle
- Ne pas exposer directement l’erreur brute au client.
- Conserver le détail pour les logs / traces, pas pour la surface publique.
- Utiliser une table mentale de mapping : validation → 400, not found → 404, auth → 401/403, panne interne → 500.
err.Error() au client finit souvent par exposer des chemins de fichiers, du SQL, des noms de services internes ou des détails de sécurité indésirables.panic n’est pas le flux normal
panic ne doit pas être une stratégie normale de contrôle de flux. Réserver cela aux erreurs réellement irréparables, aux violations d’invariants critiques, ou à des couches très basses avec garde globale maîtrisée.
func mustParseConfig(raw []byte) Config {
var cfg Config
if err := json.Unmarshal(raw, &cfg); err != nil {
panic(err)
}
return cfg
}Ce genre de pattern peut se justifier dans certains bootstrap très contrôlés, mais il doit rester rare et explicite.
recover à la frontière
func recoverMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}- Le bon usage de
recoverse situe souvent au boundary : middleware HTTP, worker supervisor, top-level command. - Le but n’est pas de normaliser la panic, mais d’éviter l’effondrement brutal d’une requête ou d’un worker.
- La panic doit être loggée avec contexte et stack si nécessaire.
panic dans les handlers HTTP ou au milieu de la logique métier pour remplacer une gestion d’erreur normale est un anti-pattern majeur.Logguer une erreur : où et quand ?
Une règle très utile en Go backend : logguer l’erreur à la frontière qui la consomme réellement, pas à chaque niveau. Sinon, on obtient des doublons, du bruit, et une observabilité polluée.
u, err := svc.GetUser(ctx, id)
if err != nil {
logger.Error("get user failed", "user_id", id, "err", err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}Pourquoi éviter le “log then return err” partout
- Duplication de logs sur une même erreur.
- Volumes inutiles.
- Difficulté à distinguer la source réelle.
- Coût cognitif en incident.
Bonne pratique de structure
- Ajouter l’opération ou le contexte utile.
- Ajouter les identifiants techniques pertinents : request ID, user ID, job ID.
- Ne pas exposer de secrets.
- Laisser la chaîne causale intacte.
logger.Error(
"payment processing failed",
"order_id", orderID,
"customer_id", customerID,
"err", err,
)Anti-patterns classiques
| Anti-pattern | Pourquoi c’est mauvais | Alternative saine |
|---|---|---|
Ignorer err | masque des défaillances réelles | traiter ou propager explicitement |
| Logger puis retourner la même erreur partout | duplique le bruit | logger au boundary |
| Transformer toute erreur en simple chaîne | perd la causalité | wrap avec %w |
| Utiliser panic dans les handlers HTTP | dégrade robustesse et clarté | retourner une erreur normale |
Comparer naïvement avec == | échoue avec wrapping | errors.Is |
| Re-wrapper sans info utile | bruit textuel | ajouter seulement un contexte pertinent |
Guide de design par couches
Questions à se poser
- Cette erreur doit-elle être identifiable plus haut ?
- Faut-il un sentinel error, un type custom, ou juste du wrapping ?
- Qui a la responsabilité du logging final ?
- Quel message est sûr pour l’utilisateur final ?
- Faut-il déclencher retry, métrique, alerte ou circuit breaker ?
Taxonomie simple et efficace
| Catégorie | Exemples | Traitement typique |
|---|---|---|
| Validation | email invalide, champ vide | 400 / message utilisateur |
| Absence | not found | 404 / fallback éventuel |
| Auth / permissions | unauthorized, forbidden | 401/403 |
| Timeout / annulation | deadline exceeded | retry / timeout response / metrics |
| Infra interne | db down, smtp fail | 500 + logs + alerting |
Code Lab — gestion d’erreurs complète et idiomatique
package main
import (
"context"
"errors"
"fmt"
"log/slog"
"net/http"
"os"
"time"
)
var ErrUserNotFound = errors.New("user not found")
type Store interface {
FindUser(ctx context.Context, id int64) (string, error)
}
type FileStore struct{}
func (s FileStore) FindUser(ctx context.Context, id int64) (string, error) {
select {
case <-ctx.Done():
return "", ctx.Err()
default:
}
if id == 404 {
return "", ErrUserNotFound
}
if id == 500 {
return "", fmt.Errorf("read backing file: %w", os.ErrPermission)
}
return "alice@example.com", nil
}
type Service struct {
store Store
}
func (s Service) GetUserEmail(ctx context.Context, id int64) (string, error) {
email, err := s.store.FindUser(ctx, id)
if err != nil {
return "", fmt.Errorf("get user email for id=%d: %w", id, err)
}
return email, nil
}
func handler(log *slog.Logger, svc Service) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
id := int64(500) // demo value
email, err := svc.GetUserEmail(ctx, id)
if err != nil {
switch {
case errors.Is(err, ErrUserNotFound):
http.Error(w, "user not found", http.StatusNotFound)
case errors.Is(err, context.DeadlineExceeded):
http.Error(w, "timeout", http.StatusGatewayTimeout)
default:
log.Error("get user email failed", "user_id", id, "err", err)
http.Error(w, "internal error", http.StatusInternalServerError)
}
return
}
_, _ = w.Write([]byte(email))
}
}
func main() {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
svc := Service{store: FileStore{}}
mux := http.NewServeMux()
mux.HandleFunc("/user-email", handler(logger, svc))
_ = http.ListenAndServe(":8080", mux)
}Ce que montre cet exemple
- sentinel error métier
- wrapping avec contexte d’opération
- prise en compte du context timeout
- mapping HTTP sécurisé
- logging unique au boundary
Évolutions possibles
- ajouter un custom error type avec code métier
- intégrer traces OpenTelemetry
- ajouter métriques par catégorie d’erreur
- différencier retryable / non-retryable
- normaliser le mapping d’erreurs avec une table centrale
Checklist de gestion d’erreurs idiomatique
Base
- toujours vérifier
err - retourner
(value, error)proprement - ajouter du contexte utile
- ne pas perdre la cause sous-jacente
Qualité
- utiliser
%wpour wrapper - utiliser
errors.Is/As - catégoriser validation / not found / timeout / interne
- éviter les logs dupliqués
Robustesse
- panic réservée aux cas vraiment exceptionnels
- recover au boundary seulement
- ne pas exposer les détails internes au client
- penser observabilité et exploitabilité
En Go, la gestion des erreurs n’est pas un détail de syntaxe : c’est une discipline d’architecture. Une erreur bien traitée doit rester visible, contextualisée, inspectable, mappable et opérationnellement utile. Lorsqu’on adopte réellement cette philosophie — wrapping pertinent,
errors.Is/As, logging au bon endroit, mapping propre aux frontières — on obtient des systèmes bien plus robustes, plus explicables et plus simples à exploiter en production.En Go, l’architecture projet ne repose pas sur un framework imposant mais sur une combinaison de conventions simples, de packages bien découpés, d’un module racine propre et d’une séparation claire entre points d’entrée, logique métier, transport, stockage et composants partagés. La vraie force vient de la lisibilité structurelle : quand on ouvre le dépôt, on doit immédiatement comprendre où se trouve l’exécutable, où se situe le domaine, où vivent les adapters et quelles parties sont internes ou réutilisables.
1) Layout simple et efficace
myapp/
cmd/
api/
main.go
worker/
main.go
internal/
app/
domain/
service/
transport/
storage/
config/
pkg/
migrations/
deploy/
scripts/
testdata/
go.mod
go.sum2) Rôle des grands répertoires
| Répertoire | Rôle | Remarque |
|---|---|---|
cmd/ | points d’entrée exécutables | un sous-dossier par binaire |
internal/ | code privé au module | non importable depuis l’extérieur |
pkg/ | code potentiellement réutilisable | à utiliser avec parcimonie |
migrations/ | schéma DB | hors logique Go pure |
deploy/ | manifests / Docker / infra | industrialisation |
testdata/ | données de test | convention Go utile |
3) Ce que doit refléter le layout
- Les dépendances réelles entre composants.
- La frontière entre privé et public.
- Les exécutables disponibles.
- Le sens du flux : transport → service → store, ou équivalent.
- Le niveau de stabilité des packages.
4) Diagramme visuel — architecture de dépôt Go
5) Réflexe sain
créer un dépôt Go avec un seul gros package ou, à l’inverse, exploser trop tôt le code en dizaines de packages abstraits. Une bonne architecture Go cherche l’équilibre : simple, lisible, évolutive.
go.mod : le cœur du module
go.mod définit le module racine, la version Go cible et les dépendances. C’est la source de vérité du projet pour la résolution des imports, la reproductibilité et la cohérence de build.
module github.com/acme/myapp
go 1.25
require (
github.com/go-chi/chi/v5 v5.2.1
github.com/jackc/pgx/v5 v5.7.2
)Rôle des lignes clés
module: identité du module.go: version cible de langage/toolchain.require: dépendances directes.- Les dépendances indirectes peuvent aussi apparaître.
go.sum : intégrité et reproductibilité
go.sum conserve les checksums des dépendances et contribue à la sécurité et à la reproductibilité. Il ne faut pas l’ignorer ni le traiter comme un simple bruit de build.
go mod tidy
go mod download
go list -m allHygiène minimale
go mod tidydoit rester propre et reproductible.- Ne pas committer un
go.modinstable ou incohérent. - Relire les changements de dépendances dans les PRs.
- Éviter les ajouts opportunistes non justifiés.
go.mod propre reflète souvent une base saine, tandis qu’un go.mod brouillon révèle vite une dette de structure.internal/ : code privé au module
Le dossier internal/ a une signification spéciale en Go : les packages qu’il contient ne peuvent être importés que depuis le module parent autorisé. C’est un excellent outil pour signaler clairement : “ce code n’est pas une API publique”.
internal/
app/
config/
domain/
service/
transport/httpapi/
storage/postgres/Pourquoi c’est précieux
- Évite l’exposition accidentelle d’APIs internes.
- Permet un refactoring plus libre.
- Aide à garder une frontière nette entre stable et privé.
- Réduit le couplage externe.
pkg/ : à utiliser avec discipline
pkg/ est souvent employé pour du code partagé potentiellement importable par d’autres modules ou par plusieurs sous-parties du dépôt. Mais il ne faut pas en faire un fourre-tout.
pkg/
httpx/
retry/
validation/
observability/Règle simple
- Si le code est strictement privé au module :
internal/. - Si le code est vraiment stable, générique et réutilisable :
pkg/. - Si vous hésitez : souvent
internal/d’abord.
pkg/ comme d’un grand tiroir “utilitaires”. Résultat : dépendances horizontales, faible cohérence, API pseudo-publique mal stabilisée. Il vaut mieux un internal/ bien structuré qu’un pkg/ fourre-tout.cmd/ : les exécutables
Le répertoire cmd/ accueille les points d’entrée. Chaque sous-dossier correspond généralement à un exécutable distinct : API HTTP, worker, CLI, migrator, scheduler, importer, etc.
cmd/
api/
main.go
worker/
main.go
migrate/
main.goRègle idiomatique
main.godoit rester mince.- Le wiring et le bootstrap peuvent vivre dans
internal/appou équivalent. - Le domaine ne doit pas dépendre de
main.
Pourquoi c’est sain
- On identifie immédiatement ce qui peut être compilé/exécuté.
- Les services partageant du code gardent des entrées séparées.
- Les responsabilités restent propres entre bootstrap et logique métier.
go build ./cmd/api
go build ./cmd/worker
go run ./cmd/migratecmd/ agit comme une carte d’exécution du projet.Découpage en couches
Transport
- HTTP
- gRPC
- CLI
- DTO / decoding / status mapping
Service
- use cases
- validation de workflow
- orchestration
- propagation du context
Storage / adapters
- database
- cache
- queue
- API externes
Exemple d’arborescence plus détaillée
internal/
domain/
user.go
order.go
errors.go
service/
user_service.go
order_service.go
transport/
httpapi/
handlers.go
middleware.go
routes.go
storage/
postgres/
user_repo.go
order_repo.go
redis/
cache.go
app/
bootstrap.go
wiring.goBut architectural
- Le transport ne contient pas la logique métier profonde.
- Le domaine reste indépendant de HTTP/SQL quand c’est pertinent.
- Les adapters peuvent être remplacés plus facilement.
- Les tests ciblent plus simplement chaque couche.
Versioning des modules
Pour les librairies publiques, respecter le semantic versioning. Pour les services internes, verrouiller les versions critiques et éviter les upgrades massifs non testés.
| Niveau | Sens |
|---|---|
| MAJOR | breaking changes |
| MINOR | nouvelles fonctionnalités compatibles |
| PATCH | correctifs compatibles |
Cas des modules v2+
module github.com/acme/tool/v2
En Go, un module majeur supérieur à v1 apparaît généralement dans le chemin d’import. Ce point est fondamental pour les bibliothèques publiques.
Politique réaliste
- Lib publique : semver strict, changelog, tags propres.
- Service interne : upgrades maîtrisés, tests de non-régression, dépendances critiques gelées si besoin.
- Éviter les “big bang upgrades” sur 40 dépendances à la fois.
git tag v1.4.2
git tag v2.0.0Gestion des dépendances
- Préférer peu de dépendances, bien choisies.
- Éviter d’introduire une lib pour un besoin minime déjà couvert par la stdlib.
- Réviser régulièrement les dépendances directes.
- Vérifier les implications des dépendances indirectes volumineuses.
go list -m all
go mod graph
go mod tidyQuestions avant d’ajouter une dépendance
- La stdlib suffit-elle déjà ?
- Le package est-il maintenu et stable ?
- Ajoute-t-il un vrai gain ou juste une abstraction de confort ?
- Le coût de long terme est-il acceptable ?
Monorepo : quand et pourquoi
Un monorepo Go peut être très efficace quand plusieurs services, librairies internes, outils de build ou agents doivent évoluer ensemble. Il offre visibilité, coordination et refactorings transverses plus simples.
repo/
services/
billing/
auth/
libs/
observability/
retry/
tools/
migrator/
codegen/Avantages
- Vue globale cohérente.
- Refactorings multi-composants facilités.
- Partage d’outils et de conventions.
- CI centralisée possible.
Risques
- Couplage implicite entre composants.
- Frontières floues si les modules sont mal gérés.
- Temps de build et CI qui grossissent.
- Tentation de dépendances internes trop faciles.
go work : workspace multi-modules
Les workspaces (go work) sont utiles pour développer plusieurs modules ensemble sans hacks locaux. Ils permettent d’indiquer à la toolchain quels modules locaux doivent être utilisés conjointement pendant le développement.
go work init ./service-a ./service-b ./shared-lib
go work use ./toolinggo.work
go.work.sumQuand c’est utile
- Monorepo multi-modules.
- Développement simultané d’une lib et d’un service qui l’utilise.
- Éviter des
replacelocaux multiples et fragiles.
Attention
go workaide le développement local, pas la politique de versioning.- Il ne faut pas oublier le comportement réel hors workspace.
- Les CI doivent refléter la stratégie voulue : mono-module, multi-module ou workspace contrôlé.
Patterns d’architecture projet
Simple service
cmd/api
internal/service
internal/storagePour petits et moyens services.
Hexagonal léger
domain
service
transport
adaptersTrès bon compromis lisibilité/testabilité.
Tooling / CLI repo
cmd/tool
internal/app
pkg/outputPratique pour outils d’infrastructure.
Règle de fond
Le meilleur layout n’est pas le plus “théorique”, mais celui qui révèle clairement comment le projet fonctionne, où vivent les décisions métier et comment les composants communiquent.
Signaux d’une mauvaise architecture
- Packages aux noms flous :
common,utils,helpers,misc. - Transport et SQL mélangés partout.
- Dépendances circulaires ou quasi-circulaires.
- Points d’entrée invisibles ou confus.
- Réutilisation non maîtrisée entre services.
Code Lab — squelette de projet Go propre
myapp/
cmd/
api/
main.go
internal/
app/
bootstrap.go
domain/
user.go
errors.go
service/
user_service.go
transport/
httpapi/
routes.go
handlers.go
storage/
memorystore/
user_repo.go
migrations/
go.mod
go.sumpackage main
import (
"log"
"net/http"
"github.com/acme/myapp/internal/app"
)
func main() {
server := app.NewHTTPServer()
log.Println("listening on :8080")
if err := http.ListenAndServe(":8080", server); err != nil {
log.Fatal(err)
}
}Pourquoi ce squelette fonctionne bien
cmd/apiidentifie clairement l’exécutable.internal/appcentralise le wiring/bootstrap.domainetservicerestent lisibles.storagepeut évoluer sans polluer le domaine.
Évolution naturelle
- ajouter un worker dans
cmd/worker - ajouter config, logger, metrics dans
internal/app - ajouter adapters Postgres / Redis
- ajouter tests et
testdata/ - ajouter Docker / CI / migrations
Checklist architecture projet Go
Module
go.modproprego.sumversionné- dépendances relues
- version Go homogène
Structure
cmd/clairinternal/structurépkg/utilisé avec discipline- couches lisibles
Scalabilité
- versioning maîtrisé
- monorepo réfléchi
go worksi utile- pas de packages fourre-tout
Une bonne architecture projet Go repose sur quelques principes très puissants : un module clair, des points d’entrée visibles, un code interne bien protégé, une séparation lisible entre transport, service, domaine et storage, et une politique de dépendances/versioning disciplinée. Ce n’est pas la sophistication qui fait la qualité d’un dépôt Go, c’est sa capacité à être compris vite, testé facilement, et maintenu longtemps sans dérive structurelle.
La concurrence en Go repose sur quelques briques extraordinairement puissantes : goroutines, channels, select, context et quelques primitives de synchronisation. Mais la facilité syntaxique ne doit jamais faire oublier la réalité : une architecture concurrente doit être bornée, supervisée, annulable et observable. Les goroutines sont légères, oui, mais pas gratuites. Les channels sont élégants, oui, mais pas magiques.
1) Idée fondamentale
En Go, la concurrence consiste moins à “faire plein de threads” qu’à structurer des tâches indépendantes, souvent I/O ou orchestration, via des unités de travail légères et des canaux de communication explicites. Le slogan historique associé à Go est souvent résumé ainsi :
2) Réalité pratique
- Une goroutine peut être lancée très facilement, mais elle doit avoir un owner clair.
- Un channel permet d’exprimer un flux, mais le producteur, le consommateur et la fermeture doivent être pensés explicitement.
- Le scheduler Go rend la concurrence simple à utiliser, mais ne corrige pas les erreurs de design.
- Les annulations, timeouts et bornes de charge sont critiques en production.
3) Cas d’usage typiques
| Cas | Motif d’usage | Pattern fréquent |
|---|---|---|
| API backend | appels concurrents à plusieurs services | fan-out/fan-in + context |
| Workers | traitement parallèle borné | worker pool |
| Pipelines | enchaînement d’étapes | stages + channels |
| Supervision | annulation et arrêt propre | context + waitgroup/errgroup |
| Timeout réseau | éviter blocages et saturation | select + ctx.Done() |
4) Diagramme visuel — topologie concurrente typique
5) Règles d’or
- Ne lancer aucune goroutine sans stratégie d’arrêt.
- Limiter explicitement le parallélisme.
- Propager le
contextdans tout ce qui fait de l’I/O ou du travail potentiellement long. - Observer et mesurer les files, délais, blocages et erreurs.
croire qu’un design concurrent est “bon” parce qu’il compile et semble rapide sur un petit test local. En réalité, ce sont les scénarios d’arrêt, de saturation, de timeout et de fuite qui déterminent la qualité d’une architecture concurrente.
Goroutines : exécution concurrente légère
go func() {
fmt.Println("running concurrently")
}()Une goroutine est une unité d’exécution gérée par le runtime Go. Elle est beaucoup plus légère qu’un thread OS classique, mais elle consomme tout de même mémoire, scheduling et ressources indirectes. Multiplier les goroutines sans contrôle peut donc devenir coûteux.
Caractéristiques importantes
- Démarrage très simple via le mot-clé
go. - Stack initiale légère et extensible.
- Ordonnancement assuré par le runtime Go.
- Parfait pour I/O, attente, orchestration et travail parallèle borné.
Mais “légère” ne veut pas dire “gratuite”
- Chaque goroutine garde une stack, un état, des références mémoire.
- Une goroutine bloquée peut retenir des ressources ou empêcher la libération de données.
- Une fuite de goroutines est une vraie fuite de système, pas un détail théorique.
for _, url := range urls {
url := url
go func() {
_ = fetch(url)
}()
}Le code ci-dessus est simple, mais il peut devenir dangereux si urls contient des milliers d’entrées ou si fetch ne respecte ni timeout ni annulation.
go func() : qui attend cette goroutine, qui peut l’annuler, et qui garantit qu’elle finira ?Channels : communication explicite
ch := make(chan int)
go func() {
ch <- 42
}()
v := <-ch
fmt.Println(v)Un channel permet d’échanger des valeurs entre goroutines de manière synchronisée. Il peut être non bufferisé (communication rendez-vous) ou bufferisé (petite file en mémoire).
Canal non bufferisé
- L’envoi bloque jusqu’à ce qu’un receveur soit prêt.
- Très utile pour exprimer une synchronisation stricte.
Canal bufferisé
jobs := make(chan int, 10)
- Permet un petit découplage entre producteur et consommateur.
- Utile pour absorber des pics ou structurer une file bornée.
Règles critiques de design
- Décider clairement qui écrit, qui lit, qui ferme.
- Ne fermer un channel que du côté producteur propriétaire.
- Utiliser
for range chquand le flux se termine naturellement par fermeture.
for job := range jobs {
fmt.Println(job)
}Types directionnels utiles
func producer(out chan<- int) { out <- 1 }
func consumer(in <-chan int) { fmt.Println(<-in) }Les channels directionnels rendent les contrats plus explicites et aident le compilateur à protéger certains usages.
select : multiplexage d’attente
select {
case msg := <-ch:
_ = msg
case <-time.After(2 * time.Second):
return errors.New("timeout")
case <-ctx.Done():
return ctx.Err()
}select permet d’attendre plusieurs opérations de communication à la fois. C’est un outil fondamental pour gérer timeouts, annulations, fan-in, supervision et comportement non bloquant.
Cas d’usage typiques
- Attendre un résultat ou un timeout.
- Prioriser une annulation de
context. - Consommer plusieurs flux concurrents.
- Construire des boucles de supervision propres.
default : avec prudence
select {
case msg := <-ch:
fmt.Println(msg)
default:
fmt.Println("nothing available")
}Ajouter un default rend le select non bloquant. Cela peut être utile, mais un mauvais usage peut produire des busy loops agressives qui consomment du CPU sans travail utile.
Boucle supervisée typique
for {
select {
case item, ok := <-jobs:
if !ok {
return
}
process(item)
case <-ctx.Done():
return
}
}select rend visibles les sorties possibles : résultat, timeout, fermeture de canal, annulation. C’est l’un des outils les plus élégants du langage quand il reste lisible.Worker pool : parallélisme borné
Le worker pool est l’un des patterns les plus utiles en Go : on limite explicitement le nombre de goroutines qui traitent une file de jobs. Cela évite le fan-out infini et stabilise l’usage CPU/mémoire/réseau.
jobs := make(chan int)
results := make(chan int)
for w := 0; w < 4; w++ {
go func() {
for j := range jobs {
results <- j * 2
}
}()
}Pourquoi c’est un bon pattern
- Le nombre de workers est contrôlé.
- Le débit est plus prévisible.
- La mémoire ne dépend pas d’un nombre infini de goroutines.
- Le pattern est simple à superviser et monitorer.
Ce qu’il faut encore ajouter pour le rendre robuste
- un
contextde cancellation - un
WaitGroupou unerrgroup - la fermeture coordonnée des channels
- une gestion explicite des erreurs
Pipelines : stages concurrents
Un pipeline découpe le traitement en étapes successives, chacune pouvant tourner dans une ou plusieurs goroutines. Chaque étape lit depuis un channel d’entrée, transforme, puis pousse vers un channel de sortie.
func stage(in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for v := range in {
out <- v * 2
}
}()
return out
}Atouts et risques
- Atouts : découpage clair, parallélisation par étape, bonne composabilité.
- Risques : channels non drainés, fuite si un stage aval s’arrête, absence de cancel global.
Règles de survie
- Toujours prévoir comment le pipeline s’arrête.
- Ajouter un
contextpour permettre une interruption coordonnée. - Éviter les buffers gigantesques qui masquent les problèmes de débit.
context : annuler, borner, propager
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()Le context est central dès qu’un travail concurrent peut être abandonné, qu’un timeout doit être appliqué, ou qu’une requête utilisateur s’arrête avant la fin du traitement.
Ce qu’il faut annuler
- appels réseau
- requêtes base de données
- travaux workers dépendants d’une requête
- pipelines dont le résultat n’est plus utile
Boucle worker annulable
for {
select {
case <-ctx.Done():
return
case job, ok := <-jobs:
if !ok {
return
}
process(job)
}
}Pourquoi c’est indispensable
- Évite les goroutines zombies.
- Réduit le gaspillage après abandon du client ou arrêt du service.
- Rend les shutdowns plus propres.
- Permet un meilleur contrôle de saturation.
Channels ne suffisent pas toujours
Les channels expriment magnifiquement des flux, mais pour certains besoins de synchronisation ou de protection mémoire, les primitives du package sync restent essentielles : WaitGroup, Mutex, RWMutex, Once, Cond, etc.
var wg sync.WaitGroup
for i := 0; i < 4; i++ {
wg.Add(1)
go func() {
defer wg.Done()
work()
}()
}
wg.Wait()Protection d’état partagé
type Counter struct {
mu sync.Mutex
n int
}
func (c *Counter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
c.n++
}Idée clé
- Channels pour flux et coordination.
- Mutex pour protéger un état partagé mutable.
- WaitGroup pour attendre une collection de travaux.
Pièges fréquents
| Erreur | Conséquence | Remède |
|---|---|---|
| goroutine leak | mémoire, CPU et I/O perdus | ctx + stratégie d’arrêt + fermeture de flux |
| channel sans consommateur | blocage | design explicite du flux |
| fan-out infini | explosion de charge | limiter concurrence |
| busy loop avec select default | CPU inutilement élevé | bloquer ou temporiser intelligemment |
| double close channel | panic | propriétaire unique de la fermeture |
| race condition | bugs intermittents | mutex, channels, design plus sûr, -race |
Autres pièges subtils
- Capturer des variables de boucle sans les réassigner proprement.
- Attendre un résultat d’une goroutine qui ne pourra jamais être envoyé.
- Ne pas drainer certains channels d’erreurs ou de résultats.
- Buffer trop petit ou trop grand, masquant le comportement réel.
Commande indispensable
go test -race ./...
Le race detector ne remplace pas une architecture correcte, mais il attrape une classe de bugs concurrents extrêmement précieux à détecter tôt.
Performance et coût réel
Une architecture concurrente n’est pas automatiquement plus rapide. Elle peut au contraire ajouter contention, scheduling, allocations, synchronisations inutiles et pressions mémoire.
Questions à se poser
- Ce travail est-il CPU-bound ou I/O-bound ?
- Le parallélisme améliore-t-il réellement le throughput ou seulement la complexité ?
- Quel est le niveau de concurrence optimal ?
- Où sont les files, les goulots et les points de blocage ?
Ce qu’il faut mesurer
- latence p50 / p95 / p99
- taille des queues
- nombre de goroutines actives
- taux d’échec / timeout / cancellation
- CPU, mémoire, blocages, contention
Code Lab — worker pool borné, supervisé et annulable
package main
import (
"context"
"fmt"
"sync"
"time"
)
func worker(ctx context.Context, id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for {
select {
case <-ctx.Done():
return
case job, ok := <-jobs:
if !ok {
return
}
// simulate work
time.Sleep(100 * time.Millisecond)
select {
case <-ctx.Done():
return
case results <- job * 2:
}
}
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
jobs := make(chan int)
results := make(chan int)
var wg sync.WaitGroup
workerCount := 4
for i := 0; i < workerCount; i++ {
wg.Add(1)
go worker(ctx, i, jobs, results, &wg)
}
go func() {
defer close(jobs)
for i := 1; i <= 10; i++ {
select {
case <-ctx.Done():
return
case jobs <- i:
}
}
}()
go func() {
wg.Wait()
close(results)
}()
for {
select {
case <-ctx.Done():
fmt.Println("stopped:", ctx.Err())
return
case r, ok := <-results:
if !ok {
fmt.Println("done")
return
}
fmt.Println("result:", r)
}
}
}Ce que montre cet exemple
- nombre de workers borné
- propagation du context
- fermeture propre des jobs
- attente structurée via
WaitGroup - fermeture coordonnée de
results
Améliorations possibles
- utiliser
errgrouppour gérer erreurs + cancel - ajouter métriques par worker
- instrumenter temps de traitement
- ajouter backpressure plus explicite
- séparer erreurs et résultats
Checklist concurrence Go
Design
- concurrence réellement nécessaire
- nombre de workers borné
- propriétaire clair des channels
- modèle d’arrêt documenté
Robustesse
contextpropagé- fermeture correcte des channels
- pas de goroutine leak
- race detector exécuté
Ops
- timeouts explicites
- métriques de débit et de file
- logs de saturation pertinents
- tests de shutdown et de charge
La concurrence en Go est l’un de ses plus grands atouts, mais aussi l’un des domaines où la discipline de design compte le plus. Les goroutines rendent le parallélisme accessible, les channels rendent les flux explicites, le select permet la supervision élégante, et le context rend l’ensemble annulable. Mais la vraie maturité consiste à bâtir des systèmes concurrents bornés, propres à arrêter, simples à observer et sûrs sous charge. C’est cette maîtrise — plus encore que la syntaxe — qui distingue un code Go “qui tourne” d’un code Go réellement prêt pour la production.
context.Context transmet l’annulation, les deadlines et quelques métadonnées requête-scopées. Il doit traverser les couches, jamais être stocké durablement dans une struct métier.
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel()
rows, err := db.QueryContext(ctx, query)srv := &http.Server{Addr: ":8080", Handler: mux}
go srv.ListenAndServe()
<-stop
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_ = srv.Shutdown(ctx)- ctx en premier argument.
- Pas de nil context.
- Pas de valeurs métier volumineuses dans ctx.
- Toujours relayer
ctx.Err().
func healthHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{"ok": true})
}Empiler les middlewares pour le logging, la corrélation, l’authentification, la récupération d’erreurs, la limitation de débit, et les timeouts.
client := &http.Client{Timeout: 5 * time.Second}
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
resp, err := client.Do(req)- Définir des timeouts serveur et client.
- Limiter la taille des bodies.
- Valider les entrées.
- Ne jamais exposer les erreurs internes brutes.
- Utiliser un schéma JSON stable et versionné.
db, err := sql.Open("postgres", dsn)
if err != nil { return err }
if err := db.PingContext(ctx); err != nil { return err }tx, err := db.BeginTx(ctx, nil)
if err != nil { return err }
defer tx.Rollback()
if _, err := tx.ExecContext(ctx, query); err != nil { return err }
return tx.Commit()| Réglage | But | Remarque |
|---|---|---|
| SetMaxOpenConns | borner concurrence DB | aligner avec capacité SGBD |
| SetMaxIdleConns | réduire churn de connexions | éviter excès d'idle |
| SetConnMaxLifetime | rotation saine | utile derrière LB/proxy |
database/sql pour le contrôle fin, sqlx pour plus de confort, GORM pour une abstraction ORM plus riche. En prod, comprendre le SQL réel reste indispensable.
func TestAdd(t *testing.T) {
cases := []struct{name string; a, b, want int}{
{"simple", 2, 3, 5},
{"zero", 0, 0, 0},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := add(tc.a, tc.b); got != tc.want { t.Fatalf("got=%d want=%d", got, tc.want) }
})
}
}go test -race ./...
Le race detector est indispensable dès qu’il y a des accès concurrents à des maps, caches, métriques ou états partagés.
func BenchmarkParse(b *testing.B) {
for i := 0; i < b.N; i++ { _ = parse(sample) }
}go test -fuzz=Fuzz -run=^$ ./...
Go est excellent pour écrire des binaires d’administration, outils DevOps, générateurs, analyseurs de logs, migrations et scanners.
name := flag.String("name", "world", "target name")
flag.Parse()
fmt.Println("hello", *name)Cobra facilite les sous-commandes, l’aide intégrée, l’autocomplétion et la structuration des CLIs plus ambitieuses.
- Sorties machine-friendly en JSON quand utile.
- Codes retour explicites.
- Logs structurés.
- Config via flags + env + fichier.
gRPC est pertinent pour les communications internes interservices, avec contrats stricts, génération de code, streaming et performances correctes.
syntax = "proto3";
package user.v1;
service UserService {
rpc GetUser(GetUserRequest) returns (User);
}Le streaming est puissant pour les flux temps réel, mais demande une discipline forte sur les timeouts, le contrôle de débit et la fermeture propre des streams.
- Deadlines partout.
- Retry sélectif, pas aveugle.
- Mapping précis des statuts.
- Tracing distribué.
Préférer des logs structurés, corrélables et parsables. Go 1.21+ apporte log/slog comme base standard moderne.
logger.Info("request completed", "path", r.URL.Path, "status", 200, "trace_id", traceID)Exposer latence, throughput, erreurs, saturation, queue depth, taille du pool DB, durée GC, et taille mémoire.
OpenTelemetry permet de relier appels HTTP, DB, gRPC et traitements internes dans une même trace distribuée.
import _ "net/http/pprof"
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()go test -bench=. -benchmem ./...
go tool pprof cpu.prof- Éviter les conversions string/[]byte inutiles.
- Préallouer les slices quand la taille est connue.
- Réduire les interfaces là où elles forcent certaines allocations.
- Observer l’escape analysis du compilateur.
Les maps ne sont pas thread-safe. Les slices sont puissantes mais demandent de comprendre capacité, append, partage de backing array et copies implicites.
| KPI | But | Action |
|---|---|---|
| p95/p99 | latence stable | profiler les hot paths |
| alloc/op | réduire pression GC | benchmem |
| goroutines count | détecter les leaks | dashboards + tests |
- Valider toutes les entrées.
- Limiter les tailles de payloads.
- Échapper correctement JSON/HTML/SQL selon le contexte.
- Interdire les URLs arbitraires non filtrées pour éviter SSRF.
Jamais de secrets dans le code ni dans l’image Docker finale. Utiliser variables d’environnement, vault ou mécanismes natifs cloud.
Configurer TLS correctement, headers de sécurité, timeouts réseau, et rotation de certificats. Éviter les clients HTTP sans bornes ni contrôles.
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o app ./cmd/api
FROM golang:1.24 AS build
WORKDIR /src
COPY . .
RUN go mod download && CGO_ENABLED=0 go build -o /out/app ./cmd/api
FROM gcr.io/distroless/static-debian12
COPY --from=build /out/app /app
ENTRYPOINT ["/app"]- Readiness/liveness probes.
- Graceful shutdown obligatoire.
- Ressources CPU/RAM réalistes.
- HPA seulement après observabilité correcte.
- gofmt, lint, tests, race.
- build reproductible.
- scan sécurité.
- image immuable.
- déploiement progressif.
Le langage Go récompense les architectures lisibles, les interfaces petites, les dépendances visibles, les flux d’erreur explicites et la simplicité opérationnelle. Les “bons patterns” Go ne sont pas forcément sophistiqués ; ils sont surtout pragmatiques, stables, observables et faciles à relire. Le langage favorise les designs où l’on comprend vite : comment la requête entre, où se trouve la logique métier, comment les dépendances sont injectées, comment les erreurs remontent et comment le système s’arrête proprement.
1) Patterns utiles fondamentaux
- Handler → service → repository.
- Interfaces minimales au point de consommation.
- Context partout aux frontières I/O.
- Workers bornés.
- Configuration explicite et immutable au runtime.
- Logging structuré au boundary.
- Erreurs wrapées avec contexte utile.
- Zero-values utiles quand possible.
2) Pourquoi ces patterns marchent bien
| Pattern | Bénéfice principal | Effet long terme |
|---|---|---|
| handler → service → repository | responsabilités claires | maintenance plus simple |
| petites interfaces | découplage ciblé | tests plus faciles |
| context I/O | timeouts et annulation | meilleure robustesse prod |
| workers bornés | charge contrôlée | stabilité sous pression |
| config explicite | startup prévisible | moins de surprises runtime |
3) Modèle mental recommandé
4) Diagramme visuel — Go idiomatique en production
5) Formule pratique
- Préférer le clair au brillant.
- Préférer la structure visible à l’abstraction prématurée.
- Préférer les primitives du langage aux frameworks envahissants.
- Préférer la robustesse opérationnelle à l’élégance théorique.
dès qu’un dépôt Go commence à ressembler à une imitation compliquée de Java, C# ou Spring avec usines, couches vides et abstractions spéculatives, il perd une grande partie de sa force naturelle.
Pattern central : boundary → service → repository
HTTP handler
|
v
Service
|
v
Repository / AdapterC’est probablement l’un des patterns les plus rentables en Go. Le handler ou boundary décode la requête, gère le transport et mappe la réponse. Le service porte le workflow métier. Le repository ou l’adapter encapsule l’accès à la DB, au cache, à une API externe ou à un message bus.
Pourquoi ce pattern est durable
- Chaque couche a un rôle lisible.
- Les tests deviennent plus simples à écrire.
- Les dépendances sont injectables via interfaces ciblées.
- Le domaine ne se retrouve pas pollué par HTTP/SQL partout.
Exemple idiomatique
type UserStore interface {
FindByID(ctx context.Context, id int64) (User, error)
}
type Service struct {
store UserStore
}
func (s Service) GetUser(ctx context.Context, id int64) (User, error) {
return s.store.FindByID(ctx, id)
}Règles de découpage
- Le handler ne doit pas contenir la logique métier profonde.
- Le service ne doit pas connaître les détails de transport HTTP.
- Le repository ne doit pas décider du statut HTTP final.
Patterns de concurrence utiles
- Worker pool borné.
- Fan-out / fan-in avec limite de parallélisme.
- Pipeline par étapes bien définies.
errgrouppour supervision + cancellation.contextpropagé systématiquement dans l’I/O.
g, ctx := errgroup.WithContext(ctx)
for _, item := range items {
item := item
g.Go(func() error {
return process(ctx, item)
})
}
if err := g.Wait(); err != nil {
return err
}Pourquoi ces patterns sont bons
- Ils bornent le travail concurrent.
- Ils rendent l’annulation explicite.
- Ils évitent les goroutines “orphelines”.
- Ils restent compatibles avec l’observabilité et le shutdown propre.
Pattern de configuration explicite
Une configuration Go saine est chargée au startup, validée une fois, transformée en struct typée, puis transmise explicitement aux composants concernés. Elle ne doit pas devenir un état global malléable pendant l’exécution.
type Config struct {
HTTPPort int
DatabaseDSN string
ReadTimeout time.Duration
WriteTimeout time.Duration
}Bootstrap propre
- charger
- valider
- assembler les dépendances
- exposer des composants prêts à l’emploi
Pourquoi ce pattern est excellent
- Le comportement du système est prévisible.
- Le startup échoue tôt si la config est invalide.
- Les tests peuvent injecter facilement une config de test.
- On évite la dérive d’un état global implicite.
cfg, err := LoadConfig()
if err != nil {
return fmt.Errorf("load config: %w", err)
}
app := NewApp(cfg)Pattern d’observabilité
- Logs structurés.
- Metrics par flux métier et technique.
- Tracing des opérations distribuées.
- Health/readiness endpoints.
- Profiling activable en environnement maîtrisé.
logger.Error(
"payment failed",
"order_id", orderID,
"user_id", userID,
"err", err,
)Pourquoi c’est un pattern et pas juste de l’ops
En Go, un bon design se voit aussi à sa capacité à être observé. Un service qui compile et répond, mais que l’on ne peut ni comprendre en incident, ni profiler, ni diagnostiquer, est un service incomplet.
Anti-patterns majeurs en Go
| Anti-pattern | Pourquoi c’est mauvais | Alternative idiomatique |
|---|---|---|
| Créer trop d’abstractions trop tôt | complexité spéculative | commencer simple, abstraire après besoin réel |
| Répliquer un style Java/C# avec usines et couches excessives | perte de lisibilité, verbosité vide | structs simples, fonctions claires, wiring explicite |
| Utiliser des channels partout | sur-complexifie le flux | mutex ou appel direct quand suffisant |
| Global state non protégé | race conditions, couplage caché | injection explicite, sync si nécessaire |
| Ignorer les timeouts et erreurs réseau | pannes silencieuses, blocages | context, timeouts, wrapping d’erreurs |
| God packages / utils fourre-tout | architecture floue | packages métier ciblés |
| Logger puis retourner partout | bruit et duplication | logguer au boundary |
| Panic comme contrôle de flux | fragilise le système | erreurs explicites |
Style guide idiomatique
- Noms courts, mais sémantiquement évidents.
- Fonctions courtes, sans imbrications excessives.
- Variables d’erreur nommées
errsauf contexte spécial. - Packages cohérents, pas de “helpers”, “misc”, “common” sans borne claire.
- Commentaires utiles, pas décoratifs.
Ce que le style Go privilégie
- Le code doit être facile à scanner visuellement.
- Le flux d’échec doit être visible.
- Le formatage automatique met tous les développeurs d’accord.
- Les fonctions doivent être compréhensibles sans gymnastique mentale.
func (s Service) CreateUser(ctx context.Context, in Input) (User, error) {
if err := in.Validate(); err != nil {
return User{}, fmt.Errorf("validate input: %w", err)
}
u, err := s.store.Save(ctx, in.ToUser())
if err != nil {
return User{}, fmt.Errorf("save user: %w", err)
}
return u, nil
}Heuristiques de code review
- Comprend-on en 30 secondes le rôle du package ?
- La fonction a-t-elle une responsabilité claire ?
- Le contexte est-il propagé dans les appels I/O ?
- Les erreurs sont-elles contextualisées sans bruit ?
- La concurrence est-elle bornée et annulable ?
- Les dépendances sont-elles injectées proprement ?
- Le logging se situe-t-il au bon niveau ?
Signaux faibles de dérive
- beaucoup de fichiers “manager”, “factory”, “builder”, “provider” sans vraie valeur
- couches vides qui ne font que passer l’appel
- mélange transport / métier / DB dans le même handler
- abstractions génériques avant tout besoin concret
- packages utilitaires devenus fourre-tout
Playbook de refactoring idiomatique
- Identifier les packages flous et les responsabilités mélangées.
- Déplacer les interfaces au point de consommation.
- Couper les “god structs” en types cohérents.
- Introduire le context là où l’I/O est longue ou critique.
- Remplacer le fan-out sauvage par un worker pool borné.
- Réduire les helpers globaux au profit de composants explicites.
Ordre conseillé
Roadmap expert
- Maîtriser
net/httpetdatabase/sql. - Devenir solide en concurrence et contexte.
- Savoir profiler avec
pprof. - Construire une API complète et observable.
- Industrialiser avec Docker + CI/CD + sécurité.
Lecture “maturité”
- Junior Go : écrit du code qui marche.
- Intermédiaire : écrit du code maintenable.
- Senior : écrit du code qui reste clair sous croissance.
- Expert : écrit des systèmes opérables, testables, stables sous charge et simples à faire évoluer.
Code Lab — pattern sain vs dérive classique
package main
import (
"context"
"errors"
"fmt"
)
type User struct {
ID int64
Email string
}
type UserStore interface {
Save(ctx context.Context, u User) error
}
type Mailer interface {
Send(to, body string) error
}
type Service struct {
store UserStore
mailer Mailer
}
func NewService(store UserStore, mailer Mailer) Service {
return Service{
store: store,
mailer: mailer,
}
}
func (s Service) Register(ctx context.Context, u User) error {
if u.Email == "" {
return errors.New("email required")
}
if err := s.store.Save(ctx, u); err != nil {
return fmt.Errorf("save user: %w", err)
}
if err := s.mailer.Send(u.Email, "welcome"); err != nil {
return fmt.Errorf("send welcome email: %w", err)
}
return nil
}Pourquoi c’est bon
- interfaces petites
- service lisible
- erreurs contextualisées
- dépendances explicites
- testabilité naturelle
Ce qu’il faut éviter à la place
- service qui connaît SQL, SMTP, HTTP et logs détaillés en même temps
- factory inutile pour fabriquer une seule struct simple
- grosse interface “AppManager” qui fait tout
- variables globales partagées partout
Checklist finale — patterns Go sains
Structure
- responsabilités séparées
- packages cohérents
- interfaces petites
- bootstrap explicite
Runtime
- context sur I/O
- timeouts explicites
- workers bornés
- shutdown propre
Qualité
- code lisible
- erreurs exploitables
- logs/metrics utiles
- pas d’abstraction prématurée
Les meilleurs patterns Go ne cherchent pas à transformer le langage en framework généraliste. Ils s’appuient au contraire sur ses forces natives : simplicité, contrats lisibles, erreurs explicites, concurrence bornée, configuration claire et observabilité sérieuse. À l’inverse, les anti-patterns les plus coûteux viennent souvent d’une volonté de sur-ingénierie ou de mimétisme avec d’autres écosystèmes. Le vrai “style expert” Go consiste à produire un code sobre, clair, opérable et durable.
Commandes cœur
go run .
go build ./...
go test ./...
go test -race ./...
go test -bench=. ./...
go mod tidy
golangci-lint runBuild ciblé
go build ./cmd/api
go run ./cmd/api
go build -o bin/api ./cmd/apiModules
go mod init github.com/acme/myapp
go mod tidy
go mod download
go list -m all
go mod graphInspection
go version
go env
go env GOMOD
go env GOPATH
go env GOMODCACHETests / bench / cover
go test ./...
go test -run TestName ./...
go test -cover ./...
go test -bench=. -benchmem ./...
go test -race ./...Nettoyage
go clean -cache
go clean -testcache
go clean -modcacheVariables
var x int
var name = "alice"
age := 42
const pi = 3.14Fonctions
func add(a, b int) int {
return a + b
}
func split(sum int) (int, int) {
x := sum / 2
return x, sum - x
}Contrôle
if err != nil {
return err
}
for i := 0; i < 10; i++ {}
for _, item := range items {}
switch status {
case "ok":
default:
}Rappels ultra rapides
if err != nil { return err }
defer cleanup()
go fn()
select { ... }
ctx, cancel := context.WithTimeout(...)
defer cancel()Collections
arr := [3]int{1,2,3}
slice := []int{1,2,3}
m := map[string]int{"a": 1}Struct
type User struct {
ID int64
Email string
Admin bool
}Init
u := User{
ID: 1,
Email: "a@example.com",
Admin: true,
}Methods
func (u User) IsAdmin() bool {
return u.Admin
}
func (u *User) Promote() {
u.Admin = true
}Receiver rule
value receiver -> read / small type
pointer receiver -> mutate / avoid copyInterfaces
type Mailer interface {
Send(to, body string) error
}Règles
small interfaces
define at point of use
composition > inheritance
internal/ for private codePattern de base
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("read %s: %w", path, err)
}Sentinel
var ErrNotFound = errors.New("not found")Inspection
if errors.Is(err, os.ErrNotExist) {}
var pathErr *os.PathError
if errors.As(err, &pathErr) {}Custom type
type ValidationError struct {
Field string
Msg string
}Rappels
wrap with %w
log at boundary
don't expose raw internal errors
panic != normal flowAnti-pattern
ignore err
log then return everywhere
panic in handlers
destroy causal chainGoroutine
go func() {
work()
}()Channel
ch := make(chan int)
go func() { ch <- 42 }()
v := <-chselect
select {
case msg := <-ch:
_ = msg
case <-time.After(2 * time.Second):
return errors.New("timeout")
case <-ctx.Done():
return ctx.Err()
}Worker pool
jobs := make(chan Job)
for i := 0; i < 4; i++ {
go worker(ctx, jobs)
}Règles de survie
bounded concurrency
owner for every goroutine
close channels explicitly
ctx everywhere on long work
go test -race ./...Quand utiliser sync
WaitGroup -> wait workers
Mutex -> protect shared state
Once -> one-time initHandler minimal
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
_ = ctx
w.WriteHeader(http.StatusOK)
}JSON input
var in Input
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
return
}JSON output
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(out)Server timeouts
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}HTTP checklist
validate input
map errors cleanly
don't leak internals
add request logging
set timeouts
graceful shutdownPattern
handler -> service -> store
database/sql
db, err := sql.Open("postgres", dsn)
if err != nil {
return err
}
defer db.Close()Query avec context
row := db.QueryRowContext(ctx,
"select email from users where id = $1", id,
)Pool tuning
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(25)
db.SetConnMaxLifetime(30 * time.Minute)
db.SetConnMaxIdleTime(5 * time.Minute)Transaction
tx, err := db.BeginTx(ctx, nil)
if err != nil { return err }
defer tx.Rollback()Règles storage
always use context
wrap queries with operation name
close rows
scan carefully
tune pool
measure slow queriesRepository interface
type UserStore interface {
FindByID(ctx context.Context, id int64) (User, error)
Save(ctx context.Context, u User) error
}Format / lint
go fmt ./...
goimports -w .
go vet ./...
golangci-lint runVulnérabilités
govulncheck ./...
Debug
dlv debug ./cmd/api
dlv test ./internal/serviceWorkspace
go work init ./service-a ./shared-lib
go work use ./toolingToolchain idéale
Go stable
VS Code + Go ext or GoLand
gofmt + goimports
golangci-lint
govulncheck
delve
CI strictProduction
timeouts HTTP
context partout
logs structurés
pprof en non-public
pool DB réglé
graceful shutdown
image minimaleShutdown
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()Docker multi-stage
FROM golang:1.25 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/api ./cmd/apiFROM gcr.io/distroless/static-debian12
COPY --from=builder /out/api /api
USER nonroot:nonroot
ENTRYPOINT ["/api"]Ops checklist
health / readiness
metrics
request IDs
timeout discipline
non-root containers
small images
dependency hygieneBench / mem
go test -bench=. -benchmem ./...
go test -cpuprofile=cpu.out ./...
go test -memprofile=mem.out ./...pprof
go tool pprof cpu.out
go tool pprof mem.outHTTP pprof
import _ "net/http/pprof"
Mesures utiles
latency p95/p99
allocations
goroutine count
db wait time
queue depth
error rate
timeouts / cancellationsLayout type
myapp/
cmd/
api/
worker/
internal/
app/
domain/
service/
transport/
storage/
pkg/
migrations/
go.mod
go.sumArchitecture utile
handler -> service -> repository
small interfaces
context on I/O
explicit config
clear boundaries
internal/ for private codeAnti-patterns
god packages
premature abstractions
panic as control flow
global mutable state
fan-out without limits
logging duplicates everywhereFlash 1
small packages
small interfaces
explicit errors
clear ownership
simple buildsFlash 2
context on every boundary
limit concurrency
close channels carefully
log at boundaries
measure before optimizingFlash 3
Go rewards:
clarity
discipline
operability
maintainability
pragmatismÉcrire du bon Go, c’est construire un code lisible, avec des erreurs explicites, des interfaces petites, des timeouts partout où il faut, une concurrence bornée, une architecture claire et une exploitation simple en production.
En Go, il vaut mieux maîtriser profondément quelques références fondamentales que collectionner des dizaines de ressources hétérogènes. Les meilleures sources sont celles qui expliquent le pourquoi du langage, ses idiomes, ses packages standards, ses patterns opérationnels et sa manière naturelle de construire des services robustes. Une très bonne stratégie consiste à partir du cœur officiel, puis à approfondir par domaines : HTTP, concurrence, base de données, observabilité, performance, sécurité, packaging et déploiement.
Références de base à toujours garder proches
- Documentation officielle Go.
- Effective Go.
- Go Proverbs / talks de Rob Pike.
- Documentation
net/http,context,database/sql,slog,pprof. - Guides de tests, sécurité, observabilité et déploiement cloud native.
Pourquoi ces références sont les bonnes
| Catégorie | Apport principal | Quand la consulter |
|---|---|---|
| Docs officielles | source de vérité du langage et des packages | en continu |
| Effective Go | style et idiomes fondamentaux | au début puis régulièrement |
| Talks Rob Pike | philosophie et vision du langage | pour comprendre le “mindset” |
| Docs packages clés | maîtrise du standard library runtime/prod | par domaine |
| Guides ops / sécurité | mise en production réelle | dès les premiers services |
Carte mentale des références
Diagramme visuel — parcours de lecture Go
Règle de bon sens
- Ne pas séparer théorie et pratique trop longtemps.
- Lire un package officiel, puis l’utiliser dans un mini projet.
- Revenir aux docs après chaque incident, bug ou benchmark surprenant.
- Construire une bibliothèque personnelle de snippets, patterns et pièges observés.
Socle officiel indispensable
- Documentation officielle Go.
- Language specification.
- Effective Go.
- Package documentation standard library.
- Release notes et évolution de la toolchain.
Pourquoi commencer ici
- Vous obtenez les conventions voulues par le langage lui-même.
- Vous évitez les surcouches communautaires parfois trop subjectives.
- Vous construisez des réflexes idiomatiques durables.
Ordre de lecture conseillé
- Tour of Go ou rappel syntaxique rapide.
- Effective Go.
- Spec par sections selon les besoins.
- Documentation détaillée des packages utilisés quotidiennement.
Packages cœur à maîtriser prioritairement
| Package | Rôle | Pourquoi le connaître à fond |
|---|---|---|
context | timeout, cancel, propagation | critique pour I/O, APIs, workers |
net/http | serveur et client HTTP | base des APIs et services |
database/sql | accès DB standard | indispensable pour la persistence sérieuse |
log/slog | logging structuré | observabilité moderne |
errors / fmt | wrapping et inspection | gestion d’erreurs idiomatique |
encoding/json | sérialisation | omniprésent en APIs |
os / io | fichiers, flux, système | fondation outillage et services |
Références concurrence à étudier
- Go Proverbs / talks de Rob Pike.
- Docs sur
context. - Docs sur
sync. - Exemples officiels de goroutines, channels et
select. - Guides sur worker pools, cancellation et supervision.
Objectifs d’apprentissage
- comprendre la différence entre parallélisme et concurrence
- maîtriser la fermeture de channels
- gérer les timeouts et annulations
- éviter les goroutine leaks
Ce qu’il faut vraiment retenir
Références HTTP / API
- Documentation
net/http. - Documentation
httptest. - Docs
contextappliquées à HTTP. - Guides sur middlewares, timeouts, graceful shutdown.
- Références sur JSON, validation, error mapping.
À étudier concrètement
- serveur HTTP natif
- client HTTP avec timeout
- handlers testables
- codes de retour et sécurité des messages d’erreur
Pourquoi c’est prioritaire
Une grande partie de l’écosystème Go tourne autour des services réseau. Maîtriser le package HTTP standard offre une base incroyablement solide, y compris lorsque l’on utilise ensuite des routeurs ou middlewares complémentaires.
net/http et comprendre ce qu’un framework ajoute, plutôt que l’inverse.Références base de données
- Documentation
database/sql. - Docs driver spécifiques selon votre SGBD.
- Guides sur pool de connexions, transactions, context deadlines.
- Lectures sur requêtes lentes, contention et stratégie d’indexation.
Sujets à creuser
- pool tuning
- gestion des rows et du scan
- transactions propres
- timeouts DB
- mapping d’erreurs de storage
Pourquoi cette lecture est décisive
Beaucoup de problèmes attribués à “Go” viennent en réalité d’une mauvaise maîtrise de la persistence : pools mal réglés, queries mal gérées, rows non fermées, erreurs SQL mal propagées ou manque d’observabilité DB.
database/sql vous prive d’un socle très important pour diagnostiquer la production.Références tests à prioriser
- Documentation
testing. httptestpour les handlers.- Benchmarks et
-benchmem. - Race detector.
- Fuzz testing quand pertinent.
Compétences attendues
- tests unitaires simples
- tests de services avec fakes
- tests HTTP
- benchmarks de hot paths
- lecture des résultats race
Le bon état d’esprit
Références performance et profiling
- Documentation
pprof. - CPU profiles, memory profiles, goroutine profiles.
- Benchmarks natifs Go.
- Docs sur allocations, GC pressure et contention.
Questions de lecture utiles
- où sont les allocations ?
- où est le temps CPU ?
- combien de goroutines vivent réellement ?
- où le programme bloque-t-il ?
Règle clé
go test -bench=. -benchmem ./... go test -cpuprofile=cpu.out ./... go test -memprofile=mem.out ./... go tool pprof cpu.out go tool pprof mem.out
Références sécurité à étudier
- Guides de sécurité applicative Go.
govulnchecket hygiène des dépendances.- Bonnes pratiques TLS / HTTP timeouts.
- Sécurité des containers et images minimales.
- Gestion des secrets et configuration runtime.
Réflexes à développer
- ne jamais exposer les erreurs internes brutes
- utiliser des timeouts réseau
- scanner les dépendances
- durcir la surface container
Zone souvent sous-estimée
Beaucoup de développeurs apprennent très bien la syntaxe Go mais trop tardivement les aspects sécurité et supply chain. Or, dans un service moderne, ces sujets doivent être étudiés en parallèle de l’architecture applicative.
Références cloud-native et déploiement
- Guides Docker multi-stage.
- Graceful shutdown et signaux système.
- Health checks, readiness, liveness.
- Logging structuré, metrics, tracing.
- Guides d’industrialisation CI/CD et images minimales.
Objectif
- passer du “ça marche en local” à “ça vit proprement en prod”
- penser déploiement, restart, rollback, saturation, shutdown
Ce qu’il faut savoir construire
Go service -> Docker multi-stage -> non-root runtime -> health endpoints -> metrics / logs -> CI/CD -> safe deployment
Parcours d’étude conseillé
Niveau 1
- tour syntaxique rapide
- Effective Go
- petits programmes CLI / fichiers / JSON
- tests unitaires simples
Niveau 2
net/httpetcontextdatabase/sql- erreurs idiomatiques
- worker pools et cancellation
Niveau 3
pprofet benchmarks- observabilité complète
- sécurité et dépendances
- Docker / CI / déploiement
Routine de progression idéale
- lire une ressource courte mais fondamentale
- implémenter un mini cas concret
- écrire tests et logs
- ajouter timeout / context / erreurs propres
- benchmarker ou profiler si besoin
Indicateur de maturité
Vous progressez vraiment lorsque vous commencez à relire les docs officielles non plus comme un débutant en syntaxe, mais comme quelqu’un qui cherche à clarifier des décisions d’architecture, de perf ou d’exploitation.
Checklist finale des références à maîtriser
Fondamentaux
- docs officielles
- Effective Go
- spec en lecture ciblée
- Go Proverbs / vision du langage
Packages clés
contextnet/httpdatabase/sqlslog/pprof
Production
- testing / race / bench
- security / vulncheck
- observability
- deployment cloud-native
Les meilleures références Go sont celles qui vous conduisent progressivement du langage vers les packages standards, puis vers la concurrence maîtrisée, la persistence, les tests, la performance, la sécurité et enfin la mise en production réelle. Maîtriser Go, ce n’est pas seulement connaître sa syntaxe ; c’est savoir relier la documentation officielle, les idiomes du langage et les exigences concrètes des systèmes robustes.
context est central en Go.Le package
context permet de propager dans l’arbre d’exécution des informations de contrôle essentielles : annulation, deadline, timeout et quelques request-scoped values. Dans un backend moderne, c’est l’un des piliers de la robustesse opérationnelle : sans contexte correctement propagé, les appels HTTP, DB, RPC, workers et goroutines continuent à tourner alors que leur résultat n’est parfois plus utile.1) Ce que context transporte réellement
- Un signal d’annulation.
- Une deadline absolue éventuelle.
- Un timeout dérivé.
- Des valeurs request-scoped limitées et ciblées.
2) Ce que context n’est pas
- Ce n’est pas un fourre-tout de dépendances globales.
- Ce n’est pas une configuration applicative permanente.
- Ce n’est pas un remplaçant de paramètres métier normaux.
- Ce n’est pas un mécanisme générique pour stocker n’importe quoi “par facilité”.
3) Le modèle mental à retenir
4) Les invariants sains
| Règle | Pourquoi |
|---|---|
Passer ctx en premier paramètre | convention claire et uniforme |
| Le propager dans tout appel I/O | éviter blocages et travail inutile |
| Annuler ce que vous créez | éviter fuites et traitements zombies |
| Limiter les valeurs au strict nécessaire | préserver lisibilité et discipline |
5) Diagramme visuel — propagation correcte du contexte
6) Pourquoi c’est indispensable en production
- Un client qui abandonne une requête ne doit pas laisser des traitements coûteux continuer inutilement.
- Un shutdown applicatif doit pouvoir interrompre proprement les travaux en vol.
- Les timeouts évitent les blocages silencieux sur services lents ou indisponibles.
passer le
context seulement à certains appels et l’oublier dans le reste de la chaîne. Le résultat est un faux sentiment de robustesse : une partie du flux est annulable, l’autre continue à consommer CPU, DB ou réseau en arrière-plan.Créer un timeout
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel()
WithTimeout crée un contexte enfant qui sera annulé automatiquement après une durée donnée. C’est très utile pour borner un appel externe ou un bloc de travail précis.
Créer une deadline absolue
deadline := time.Now().Add(2 * time.Second) ctx, cancel := context.WithDeadline(context.Background(), deadline) defer cancel()
Choisir timeout ou deadline
- Timeout : durée relative, le plus fréquent.
- Deadline : échéance absolue, pratique quand elle est déjà connue plus haut.
Lecture des erreurs associées
err := doWork(ctx)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
// timeout
}
if errors.Is(err, context.Canceled) {
// cancellation
}
}Bonne pratique
- Définir des timeouts réalistes par type d’opération.
- Éviter les timeouts globaux arbitrairement trop courts ou trop longs.
- Documenter les budgets de temps critiques : HTTP, DB, appels sortants.
Propagation correcte
func (s Service) GetUser(ctx context.Context, id int64) (User, error) {
return s.store.FindByID(ctx, id)
}Le contexte doit être propagé sans être recréé inutilement à chaque couche. Si un handler reçoit r.Context(), le service, le repository et les appels HTTP/DB descendants doivent recevoir ce même contexte, ou un dérivé plus strict si nécessaire.
Convention de signature
func DoSomething(ctx context.Context, arg string) error
ctxpremier argument.- Nom court standard
ctx. - Ne jamais stocker un contexte dans une struct pour un usage permanent.
Quand dériver un contexte
- Ajouter un timeout spécifique plus court.
- Créer un sous-travail annulable séparément.
- Associer une valeur request-scoped pertinente.
childCtx, cancel := context.WithTimeout(ctx, 500*time.Millisecond) defer cancel() err := callExternal(childCtx)
context.Background() au milieu d’un traitement HTTP ou métier est souvent une faute grave : vous coupez alors le lien avec l’annulation, le shutdown et les métadonnées de la requête d’origine.Request-scoped values : usage légitime
Les valeurs de contexte sont utiles pour quelques informations transverses et légères, liées à une requête ou à une exécution : request ID, trace ID, identité authentifiée, tenant, corrélation technique.
type ctxKey string const requestIDKey ctxKey = "request_id" ctx = context.WithValue(ctx, requestIDKey, "req-123")
Lecture
if v := ctx.Value(requestIDKey); v != nil {
requestID, _ := v.(string)
_ = requestID
}À ne pas faire
- Y stocker une config globale.
- Y stocker un logger unique central si d’autres approches plus explicites sont possibles.
- Y placer des objets lourds ou métiers arbitraires.
- Transformer le contexte en sac universel pour éviter de passer des paramètres normaux.
Règle de discipline
HTTP entrant : le contexte de la requête
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
_ = ctx
}Le contexte porté par *http.Request est la base naturelle à propager. Il est annulé lorsque le client se déconnecte, lorsque le serveur annule le traitement ou lorsque le handler termine dans certains scénarios supervisés.
HTTP sortant avec contexte
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return err
}
resp, err := httpClient.Do(req)Pourquoi c’est si important
- Un appel downstream ne doit pas survivre indéfiniment à la requête amont.
- Les budgets de temps doivent se propager entre services.
- Les annulations client doivent interrompre les appels réseau inutiles.
NewRequestWithContext dans un service backend est souvent un oubli coûteux. Le code “marche”, mais devient mauvais sous timeout, sous shutdown ou sous déconnexion client.Base de données avec contexte
row := db.QueryRowContext(ctx,
"select email from users where id = $1",
id,
)rows, err := db.QueryContext(ctx, query)
if err != nil {
return fmt.Errorf("query users: %w", err)
}
defer rows.Close()Pourquoi
- Les requêtes longues peuvent être stoppées si la requête amont est annulée.
- Le pool de connexions est moins exposé à des travaux devenus inutiles.
- Les budgets de temps deviennent cohérents entre HTTP, service et DB.
Autres opérations I/O
- Clients RPC.
- Queues et consumers.
- Caches réseau.
- Appels à APIs externes.
- Opérations de fichiers longues si votre abstraction le permet.
context.Goroutines annulables
go func() {
for {
select {
case <-ctx.Done():
return
case job := <-jobs:
_ = job
}
}
}()Une goroutine qui dépend d’une requête, d’un batch, d’un worker supervisor ou d’un shutdown doit écouter ctx.Done(). Sinon, elle risque de continuer à travailler alors que son résultat n’est plus attendu.
Pattern recommandé
Règle utile
- Chaque goroutine doit avoir un owner.
- Chaque worker doit savoir quand s’arrêter.
- L’arrêt doit drainer ou fermer proprement les flux concernés.
Shutdown propre
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
return err
}Le shutdown propre consiste à annoncer l’arrêt, arrêter d’accepter de nouveaux travaux, laisser les requêtes/traitements en vol se terminer ou s’annuler proprement, puis fermer les ressources.
Ordre logique de fermeture
- capturer le signal
- bloquer les nouvelles entrées
- annuler ou laisser finir les traitements en cours
- fermer serveurs, workers, DB, queues selon politique
Erreurs liées au contexte
if errors.Is(err, context.DeadlineExceeded) {
// timeout
}
if errors.Is(err, context.Canceled) {
// cancellation
}Lecture métier / opérationnelle
DeadlineExceededsignifie généralement que le budget de temps a été consommé.Canceledsignifie qu’un parent a demandé l’arrêt : client parti, shutdown, parent annulé, etc.
Mapping fréquent
| Cas | Traitement typique |
|---|---|
| timeout downstream | gateway timeout / retry partiel / métrique |
| annulation client | arrêt silencieux + log technique léger |
| shutdown | arrêt propre des workers et handlers |
Anti-patterns à éviter absolument
| Anti-pattern | Pourquoi c’est mauvais | Alternative |
|---|---|---|
repartir de context.Background() au milieu d’un flux | coupe l’annulation amont | propager le ctx reçu ou en dériver un enfant |
ne jamais appeler cancel() | garde des ressources inutilement | defer cancel() dès création |
| stocker le contexte dans une struct long terme | mauvaise sémantique, confusion de durée de vie | le passer explicitement aux appels |
| mettre des objets métiers lourds en value | contexte devient fourre-tout | passer des paramètres explicites |
ignorer ctx.Done() dans les workers | travail zombie / fuite | écoute active de l’annulation |
context est simple à utiliser mais facile à banaliser. Or un mauvais usage de context ne casse pas toujours le build ; il casse surtout la robustesse du système au mauvais moment.Code Lab — HTTP + service + DB + shutdown avec contexte
package main
import (
"context"
"database/sql"
"errors"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
type Store struct {
db *sql.DB
}
func (s Store) FindUserEmail(ctx context.Context, id int64) (string, error) {
row := s.db.QueryRowContext(ctx, "select email from users where id = $1", id)
var email string
if err := row.Scan(&email); err != nil {
return "", fmt.Errorf("scan user email: %w", err)
}
return email, nil
}
type Service struct {
store Store
}
func (s Service) GetUserEmail(ctx context.Context, id int64) (string, error) {
childCtx, cancel := context.WithTimeout(ctx, 800*time.Millisecond)
defer cancel()
email, err := s.store.FindUserEmail(childCtx, id)
if err != nil {
return "", fmt.Errorf("get user email id=%d: %w", id, err)
}
return email, nil
}
func main() {
db, _ := sql.Open("postgres", "postgres://demo")
svc := Service{store: Store{db: db}}
mux := http.NewServeMux()
mux.HandleFunc("/user", func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
email, err := svc.GetUserEmail(ctx, 42)
if err != nil {
switch {
case errors.Is(err, context.DeadlineExceeded):
http.Error(w, "timeout", http.StatusGatewayTimeout)
case errors.Is(err, context.Canceled):
http.Error(w, "canceled", http.StatusRequestTimeout)
default:
http.Error(w, "internal error", http.StatusInternalServerError)
}
return
}
_, _ = w.Write([]byte(email))
})
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}
go func() {
log.Println("server listening on :8080")
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}()
sigCtx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
<-sigCtx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Printf("shutdown failed: %v", err)
}
_ = db.Close()
_ = os.Stdout.Sync()
}Ce que montre cet exemple
- propagation du contexte HTTP entrant
- timeout plus strict au niveau service
- Query DB avec
QueryRowContext - mapping des erreurs de contexte
- shutdown propre au signal système
Améliorations possibles
- ajouter request ID en value
- ajouter logs structurés et traces
- gérer un worker pool annulable
- ajouter retries sélectifs sur certains appels sortants
Checklist Context & Cancellation
Conception
ctxpremier paramètre- propagation dans toute opération I/O
- pas de
Background()sauvage au milieu du flux - values rares et techniques
Runtime
- timeouts explicites
defer cancel()sur contextes créés- workers à l’écoute de
ctx.Done() - mapping propre de
DeadlineExceeded/Canceled
Ops
- shutdown propre sur signaux
- pas de travail zombie après déconnexion client
- logs et métriques sur timeouts/cancellations
- tests de charge et d’arrêt
Le package
context est l’un des mécanismes les plus importants de Go moderne. Bien utilisé, il relie en une seule chaîne la deadline, l’annulation, la propagation inter-couches, le shutdown propre et une part mesurée des request-scoped values. En pratique, maîtriser context revient à transformer un service simplement “fonctionnel” en un système réellement borné, annulable, plus sûr sous charge et plus propre à exploiter.Go est particulièrement solide pour construire des APIs HTTP et REST grâce à
net/http, à la simplicité de sa standard library, à la propagation native du context, à la sérialisation JSON efficace et à un modèle de handlers extrêmement lisible. La vraie force n’est pas seulement la vitesse d’exécution, mais la capacité à construire des services simples, testables, observables et robustes, sans dépendre d’un framework géant.1) Les briques fondamentales
- net/http pour le serveur et le client HTTP.
- json pour sérialiser et désérialiser les payloads.
- context pour deadlines, cancellation et propagation.
- middleware pour logging, auth, rate limiting, recovery.
- handlers courts et focalisés.
- services pour la logique métier.
- timeouts et erreurs bien mappées.
2) Architecture REST idiomatique
3) Ce que doit faire un bon handler
| Responsabilité | Oui / Non |
|---|---|
| Lire et décoder la requête | Oui |
| Valider l’entrée | Oui |
| Appeler le service métier | Oui |
| Faire tout le SQL directement | Non, sauf cas trivial ultra local |
| Décider du code HTTP final | Oui |
| Exposer l’erreur interne brute au client | Non |
4) Diagramme visuel — chaîne complète d’une API Go
5) Objectif réel
- Répondre vite et proprement.
- Échouer clairement et sûrement.
- Se laisser diagnostiquer en prod.
- Rester simple à faire évoluer.
surcharger les handlers avec validation, SQL, logique métier, auth, logs détaillés, retries et mapping d’erreurs au même endroit. Une API Go saine distribue clairement ces responsabilités.
net/http : socle natif très puissant
mux := http.NewServeMux()
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
})La standard library suffit largement pour beaucoup d’APIs. Elle fournit serveur, client, handlers, middleware chainables, tests HTTP et propagation de contexte.
Serveur typique
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}Pourquoi commencer par la stdlib
- Compréhension profonde du modèle HTTP Go.
- Moins de magie cachée.
- Interopérabilité élevée avec les middlewares.
- Base très stable et très bien documentée.
Quand ajouter un routeur externe
- Si vous voulez un routing plus avancé.
- Si les path params complexes deviennent nombreux.
- Si votre équipe a standardisé un composant précis.
net/http. C’est le socle réel de l’écosystème.Handlers : courts, explicites, testables
func (a App) createUser(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
_ = ctx
}Responsabilités saines d’un handler
- Lire les paramètres de route, query et body.
- Valider l’entrée ou déclencher la validation.
- Appeler un service.
- Mapper le résultat en JSON + status code.
- Ne pas contenir la logique métier profonde.
Exemple simple
func (a App) healthz(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"status":"ok"}`))
}Convention utile
- Un handler par responsabilité claire.
- Pas de fonction de 300 lignes.
- Décoder, valider, appeler, répondre.
Décoder JSON
var in struct {
Name string `json:"name"`
Email string `json:"email"`
}
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
return
}Encoder JSON
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(out)Bonnes pratiques JSON
- Définir des DTOs clairs.
- Éviter de mélanger directement modèles DB et payloads publics quand les contraintes divergent.
- Valider après décodage.
- Contrôler les champs exposés.
type UserResponse struct {
ID int64 `json:"id"`
Email string `json:"email"`
}Validation d’entrée
if strings.TrimSpace(in.Name) == "" {
http.Error(w, "name is required", http.StatusBadRequest)
return
}
if !strings.Contains(in.Email, "@") {
http.Error(w, "invalid email", http.StatusBadRequest)
return
}La validation peut rester simple pour de nombreux cas. L’essentiel est qu’elle soit cohérente, centralisée à un niveau clair et qu’elle différencie bien les erreurs de client des erreurs serveur.
Stratégies possibles
- Validation légère dans le handler.
- Validation portée par le DTO ou le service.
- Agrégation d’erreurs de champ pour les payloads plus riches.
Principe important
Middlewares utiles
- logging de requête
- recovery
- authentification
- rate limiting
- request ID / correlation ID
- CORS si nécessaire
func logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
_ = start
})
}Ordre important
L’ordre de la chaîne middleware change le comportement réel. Par exemple, le recovery doit souvent entourer l’ensemble, et le request ID doit être disponible avant les logs.
Auth : séparation claire
L’authentification et l’autorisation doivent être visibles, compréhensibles et testables. Une bonne pratique est de faire l’authentification en middleware, puis de laisser le handler ou le service appliquer les règles métier d’autorisation fines.
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}Règles saines
- 401 pour absence/échec d’authentification.
- 403 pour utilisateur authentifié mais non autorisé.
- Ne pas fuiter trop d’informations sur la raison exacte.
- Propager une identité proprement au service si nécessaire.
Timeouts serveur
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}Contexte de requête
ctx := r.Context()
Toute requête descendante — DB, HTTP externe, queue, cache — doit idéalement être branchée sur ce contexte, ou sur un enfant plus strict si besoin.
Pourquoi c’est critique
- Protège contre les connexions lentes.
- Réduit le travail inutile après déconnexion client.
- Évite les blocages infinis côté downstream.
- Facilite le shutdown propre.
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
Mapper proprement les erreurs
switch {
case errors.Is(err, ErrValidation):
http.Error(w, "invalid input", http.StatusBadRequest)
case errors.Is(err, ErrNotFound):
http.Error(w, "not found", http.StatusNotFound)
default:
http.Error(w, "internal error", http.StatusInternalServerError)
}Table mentale utile
| Catégorie | Status typique |
|---|---|
| validation | 400 |
| unauthorized | 401 |
| forbidden | 403 |
| not found | 404 |
| conflict | 409 |
| timeout downstream | 504 ou mapping spécifique |
| erreur interne | 500 |
Règle essentielle
- Ne jamais exposer les détails internes complets au client.
- Garder les détails riches pour logs, traces et metrics.
- Répondre avec un message simple, stable et sûr.
Tester les handlers avec httptest
req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
rr := httptest.NewRecorder()
handler(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", rr.Code)
}Ce qu’il faut observer en production
- latence p50 / p95 / p99
- codes de retour
- taux d’erreur
- timeouts et cancellations
- trafic par route
Logging structuré HTTP
logger.Info(
"http request",
"method", r.Method,
"path", r.URL.Path,
"status", status,
"duration_ms", duration.Milliseconds(),
)Observabilité minimale
- request ID
- logs structurés
- metrics par route et code HTTP
- tracing sur opérations critiques
Code Lab — mini API REST propre en Go
package main
import (
"context"
"encoding/json"
"errors"
"log/slog"
"net/http"
"os"
"strings"
"time"
)
var ErrValidation = errors.New("validation error")
type User struct {
ID int64 `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
type Store interface {
Save(ctx context.Context, u User) (User, error)
}
type MemoryStore struct {
nextID int64
items []User
}
func (m *MemoryStore) Save(ctx context.Context, u User) (User, error) {
select {
case <-ctx.Done():
return User{}, ctx.Err()
default:
}
m.nextID++
u.ID = m.nextID
m.items = append(m.items, u)
return u, nil
}
type Service struct {
store Store
}
func (s Service) CreateUser(ctx context.Context, name, email string) (User, error) {
if strings.TrimSpace(name) == "" || !strings.Contains(email, "@") {
return User{}, ErrValidation
}
return s.store.Save(ctx, User{Name: name, Email: email})
}
type App struct {
svc Service
log *slog.Logger
}
func (a App) createUser(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
var in struct {
Name string `json:"name"`
Email string `json:"email"`
}
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
return
}
u, err := a.svc.CreateUser(ctx, in.Name, in.Email)
if err != nil {
switch {
case errors.Is(err, ErrValidation):
http.Error(w, "invalid input", http.StatusBadRequest)
case errors.Is(err, context.DeadlineExceeded):
http.Error(w, "timeout", http.StatusGatewayTimeout)
default:
a.log.Error("create user failed", "err", err)
http.Error(w, "internal error", http.StatusInternalServerError)
}
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(u)
}
func loggingMiddleware(logger *slog.Logger, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
logger.Info("http request",
"method", r.Method,
"path", r.URL.Path,
"duration_ms", time.Since(start).Milliseconds(),
)
})
}
func recoverMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
app := App{
svc: Service{store: &MemoryStore{}},
log: logger,
}
mux := http.NewServeMux()
mux.HandleFunc("/users", app.createUser)
handler := recoverMiddleware(loggingMiddleware(logger, mux))
srv := &http.Server{
Addr: ":8080",
Handler: handler,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}
logger.Info("server starting", "addr", srv.Addr)
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
logger.Error("server failed", "err", err)
}
}Ce que montre cet exemple
- handler court
- validation explicite
- service séparé
- storage injecté
- JSON propre
- timeouts + context
- logging + recovery middleware
Améliorations possibles
- ajouter auth middleware
- ajouter request ID
- ajouter tests
httptest - ajouter métriques Prometheus
- ajouter graceful shutdown
Checklist API HTTP Go
Structure
- handler court
- service séparé
- DTOs JSON clairs
- middlewares transverses propres
Robustesse
- validation explicite
- timeouts serveur
- context propagé
- mapping d’erreurs propre
Production
- logs structurés
- tests HTTP
- metrics et traces
- shutdown propre
Construire une bonne API REST en Go consiste à tirer parti de
net/http, du context, des middlewares bien ordonnés, d’une sérialisation JSON propre, d’une validation explicite et d’un mapping d’erreurs sûr. Le résultat recherché n’est pas seulement une API “qui répond”, mais une API lisible, testable, observables et robuste sous charge.