← Contextes /
conventions-go-modernes.md 330 lignes · 8.9 KB
Personnaliser Télécharger
# 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)