# 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)