# CLAUDE.md — Conventions Go 2025-2026
> Contexte spécialisé pour Claude Code. Coller ce fichier à la racine du projet pour appliquer les conventions Go modernes issues des meilleures codebases (Tailscale, Caddy, pgx v6).
---
## Section 1 : Logging avec slog (Go 1.21+)
`slog` remplace logrus et zap. C'est dans la stdlib — aucune dépendance à installer.
**Pattern : logger enrichi par opération, pas logger global.**
```go
// ✅ Logger enrichi par contexte d'opération
func processJob(ctx context.Context, jobID, model string) {
log := slog.With("job_id", jobID, "model", model)
log.Info("starting")
// ... traitement
log.Error("failed", "error", err)
}
// ✅ Logging structuré avec valeurs typées
slog.Info("User created",
"user_id", user.ID,
"email", user.Email,
"duration_ms", elapsed.Milliseconds())
slog.Error("Failed item",
"item_id", item.ID,
"error", err,
"retry_count", retries)
// ❌ Ne jamais faire dans une librairie
slog.SetDefault(myLogger) // écrase la config de l'app qui utilise la lib
// ❌ String formatting — illisible, non-searchable
log.Printf("User %s created with %s", user.ID, user.Email)
```
**Règle** : dans les librairies, ne jamais appeler `slog.SetDefault()`. C'est le rôle de `main.go`.
---
## Section 2 : Error handling — wrapping, Join, sentinels
**Wrapping systématique avec `%w` :**
```go
// ✅ Toujours wrapper avec contexte
return fmt.Errorf("create job: %w", err)
// ❌ Perdre le contexte
return err
```
**`errors.Join` pour la validation multi-champs (Go 1.20+) :**
```go
// ✅ Multi-erreurs sans lib externe
errs := []error{}
errs = append(errs, validatePrompt(r.Prompt))
errs = append(errs, validateModel(r.Model))
return errors.Join(errs...) // nil si tous nil, combiné sinon
```
**Sentinel errors — le pattern "not found" :**
```go
var ErrJobNotFound = errors.New("job not found")
// ❌ Anti-pattern — ambigu, impossible à tester proprement
func (s *Store) Get(id string) (*Job, error) {
return nil, nil // job absent = nil, nil ← impossible à distinguer d'un succès
}
// ✅ Explicite et testable avec errors.Is
func (s *Store) Get(id string) (*Job, error) {
return nil, ErrJobNotFound
}
// Côté appelant — propre, sans comparaison de strings
if errors.Is(err, ErrJobNotFound) {
return http.StatusNotFound, nil
}
```
**Resilient execution — continuer malgré les erreurs partielles :**
```go
// ✅ Partial success > total failure
jobErrors := &JobErrors{}
successCount := 0
for _, item := range items {
if err := process(item); err != nil {
jobErrors.Add(JobError{Item: item.ID, Error: err})
slog.Error("Failed item, continuing", "item", item.ID, "error", err)
continue // ne jamais stopper sur une erreur partielle
}
successCount++
}
if successCount == 0 && len(items) > 0 {
return 0, fmt.Errorf("all %d items failed", len(items))
}
```
---
## Section 3 : Context — propagation et WithoutCancel
**Règle : toujours propager le context, sans exception.**
```go
// ✅ Context propagé sur toutes les opérations I/O
func Service(ctx context.Context, id string) error {
user, err := userRepo.Get(ctx, id)
if err != nil { return err }
return processUser(ctx, user)
}
// ❌ Ne jamais créer un nouveau context ou ignorer l'existant
func BadService(ctx context.Context, id string) error {
user, err := userRepo.Get(context.Background(), id) // perd la trace, le user ID, etc.
return processUser(ctx, user)
}
```
**`context.WithoutCancel` (Go 1.21) — fire-and-forget propre :**
```go
// ✅ Webhooks, notifications, opérations de fond
go webhook.Send(context.WithoutCancel(ctx), url, payload)
// Conserve les valeurs du contexte (trace ID, user ID...)
// mais n'est pas annulé si la requête HTTP d'origine expire
// ❌ Avant Go 1.21 — perd toutes les valeurs du contexte parent
go webhook.Send(context.Background(), url, payload)
```
**Timeout sur toutes les opérations externes :**
```go
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
```
---
## Section 4 : Itérateurs iter.Seq2 (Go 1.23+)
Adopté par pgx v6 et sqlc. Lazy, composable, stop propre sur `break`.
```go
// ❌ Avant Go 1.23 — verbeux, charge tout en mémoire
rows, err := s.db.QueryContext(ctx, `SELECT id, status, prompt FROM jobs`)
if err != nil {
return fmt.Errorf("query jobs: %w", err)
}
defer rows.Close()
for rows.Next() {
var j Job
if err := rows.Scan(&j.ID, &j.Status, &j.Prompt); err != nil {
return fmt.Errorf("scan job: %w", err)
}
jobs = append(jobs, j)
}
// ✅ Go 1.23+ — lazy, composable, gestion d'erreur explicite
func (s *Store) Jobs(ctx context.Context) iter.Seq2[*Job, error] {
return func(yield func(*Job, error) bool) {
rows, err := s.db.QueryContext(ctx, `SELECT id, status, prompt FROM jobs`)
if err != nil {
yield(nil, err)
return
}
defer rows.Close()
for rows.Next() {
j := &Job{}
if err := rows.Scan(&j.ID, &j.Status, &j.Prompt); err != nil {
yield(nil, err)
return
}
if !yield(j, nil) {
return // appelant a fait break — stop propre
}
}
}
}
// Usage
for j, err := range store.Jobs(ctx) {
if err != nil {
return fmt.Errorf("list jobs: %w", err)
}
// traiter j
}
```
---
## Section 5 : Testing — synctest, t.Parallel, t.Cleanup
**`testing/synctest` (Go 1.24) — fin des `time.Sleep` dans les tests :**
```go
// ❌ Sleep hacky, flaky sur machines lentes
go worker.processJob(ctx, "job-1")
time.Sleep(100 * time.Millisecond)
assert.Equal(t, "completed", store.GetStatus("job-1"))
// ✅ Go 1.24 — synctest contrôle le temps virtuel
synctest.Run(func() {
go worker.processJob(ctx, "job-1")
synctest.Wait() // attend que toutes les goroutines soient bloquées ou terminées
assert.Equal(t, "completed", store.GetStatus("job-1"))
})
```
**Table-driven tests avec `t.Parallel()` systématique :**
```go
func TestValidate(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input string
wantErr bool
}{
{"valid", "claude-3-5-sonnet", false},
{"empty", "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel() // révèle les data races
err := validateModel(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("got %v, want err=%v", err, tt.wantErr)
}
})
}
}
// t.Cleanup — plus propre que defer dans TestXxx
store := newTestStore(t)
t.Cleanup(func() { store.Close() })
```
---
## Section 6 : Mocks et go:generate
mockery v3 est le standard de facto. Les mocks sont versionnés avec le code.
```go
//go:generate mockery --name=JobStore --output=mocks --outpkg=mocks
type JobStore interface {
Get(ctx context.Context, id string) (*Job, error)
Save(ctx context.Context, job *Job) error
}
```
```
go generate ./...
```
Le `go generate` en CI vérifie que les mocks sont à jour — pas de script externe.
---
## Section 7 : Tooling via go.mod (Go 1.24+)
Fini le `curl | sh` dans les Makefiles. La directive `tool` versionne les outils de dev.
```
// go.mod
tool (
github.com/golangci/golangci-lint/cmd/golangci-lint v1.62.0
golang.org/x/vuln/cmd/govulncheck v1.1.3
)
```
```
// Utilisation — même version partout
go tool golangci-lint run
go tool govulncheck ./...
```
**Bénéfice** : tout le monde utilise la même version. Plus de "ça passe chez moi".
---
## Section 8 : Dépendances — ce qu'on n'installe plus
Les meilleurs projets Go 2025 ont 3 à 5 dépendances directes maximum.
| Abandonné / évité | Remplacé par |
|---|---|
| `github.com/pkg/errors` | stdlib `errors` + `fmt.Errorf("%w")` |
| `github.com/sirupsen/logrus` | `slog` |
| `go.uber.org/zap` | `slog` |
| `gorilla/mux` | `net/http` routing stdlib (Go 1.22) |
| `testify/assert` (nouveaux projets) | stdlib `testing` |
| `cobra` pour petits CLIs | `flag` + sous-commandes manuelles |
**Routing stdlib Go 1.22 :**
```go
// ✅ Plus besoin de gorilla/mux
mux := http.NewServeMux()
mux.HandleFunc("GET /api/jobs/{id}", handleGetJob)
mux.HandleFunc("POST /api/jobs", handleCreateJob)
```
---
## Section 9 : Checklist avant commit
- [ ] Erreurs wrappées avec `fmt.Errorf("context: %w", err)` — jamais retournées nues
- [ ] `nil, nil` pour "not found" remplacé par `nil, ErrXxx`
- [ ] Context propagé sur toutes les opérations I/O
- [ ] `context.WithoutCancel` utilisé pour les opérations fire-and-forget
- [ ] `slog` utilisé, pas `log.Printf` ni logrus/zap
- [ ] Goroutines peuvent terminer (select + `ctx.Done()`)
- [ ] Channels fermés avec `defer close(ch)`
- [ ] `t.Parallel()` sur les nouveaux tests unitaires
- [ ] `govulncheck ./...` passe sans vulnérabilité critique
- [ ] `go.mod` : dépendances directes justifiées (pas de lib utilitaire disponible en stdlib)