Use Cases (Mis à jour: 02/06/2026)

Developpement Go avec Claude Code : go.mod, tests, concurrence et race detector

Guide pratique Claude Code pour Go : repo map, go.mod, go work, API/CLI, tests, context, concurrence, benchmarks.

Developpement Go avec Claude Code : go.mod, tests, concurrence et race detector

Quand vous utilisez Claude Code pour developper en Go, la demande risquee est “cree une API” ou “ajoute des tests” sans contexte du depot. Vous pouvez obtenir du code qui s execute, mais qui s eloigne du go.mod existant, des frontieres de packages, de la politique d erreurs, de la propagation de context ou de la securite de concurrence. Go semble simple, donc ces problemes se cachent facilement dans de petits diffs.

Ce guide considere Claude Code comme un agent de developpement Go, pas seulement comme un generateur de code. Le flux couvre la cartographie du depot, les decisions de module et de workspace, les changements API/CLI, les table-driven tests, l emballage des erreurs, l annulation avec context, les pieges de concurrence, le race detector, les benchmarks et les prompts surs. Un module est l unite de gestion des dependances, un workspace est un support pour travailler avec plusieurs modules locaux, et context transmet annulation et deadlines aux appels en aval.

Masa a teste ce flux sur une petite API de taches en Go. Le premier prompt disait seulement “ajoute un handler” ; le code fonctionnait localement, mais il n y avait pas de tests, l annulation etait floue et une goroutine pouvait ecrire dans un map partage. En demandant d abord “lis le repo et verifie avec go test -race”, la revue est devenue beaucoup plus concrete.

Commencer par cartographier le depot

La premiere tache Go n est pas de generer du code. C est de faire une carte : quels dossiers contiennent des commandes, quels packages sont internes, quel go.mod controle les dependances et ce que CI verifie deja. Demandez a Claude Code une premiere passe en lecture seule.

pwd
find . -name go.mod -o -name go.work -o -name "*.go" | sort | sed -n '1,120p'
go env GOMOD GOWORK
go list -m
go list ./...
go test ./...

Utilisez un prompt limite :

Inspecte ce depot Go. Ne modifie pas encore les fichiers.
Rapporte:
- Si go.mod et go.work existent
- Le role de cmd, internal, pkg, api, migrations et testdata
- Les types et fonctions publics dont le changement casserait la compatibilite
- Le style actuel de gestion des erreurs
- Les frontieres qui acceptent context.Context
- Le resultat de go test ./...
- Le plus petit ensemble de fichiers qu il est sur de modifier ensuite

Comme references officielles, utilisez Organizing a Go module pour l organisation et Go Modules Reference pour les dependances. Pour Claude Code, gardez Claude Code overview sous la main.

flowchart LR
  A["repo map"] --> B["go.mod / go.work"]
  B --> C["API or CLI change"]
  C --> D["table-driven tests"]
  D --> E["go test -race"]
  E --> F["benchmark and review"]

Pour les flux proches, consultez cartographier un codebase existant et bonnes pratiques CLAUDE.md. En Go, la meme regle tient : fixer l accord de travail avant de demander l implementation.

Ne pas laisser go.mod et go work diverger

go.mod enregistre le chemin du module, la version de Go et les dependances. Une petite application a souvent besoin d un seul go.mod. Un depot multi-module peut utiliser go.work pour que la commande go voie plusieurs modules locaux en meme temps. Le tutoriel officiel est Getting started with multi-module workspaces.

Avant d autoriser Claude Code a ajouter des dependances, inspectez l etat courant :

go env GOMOD GOWORK
go list -m all
go mod tidy
go test ./...

Creez un workspace seulement si vous devez vraiment modifier plusieurs modules ensemble :

mkdir -p services/taskapi tools/taskctl
cd services/taskapi
go mod init example.com/acme/taskapi
cd ../..
cd tools/taskctl
go mod init example.com/acme/taskctl
cd ../..
go work init ./services/taskapi ./tools/taskctl
go work use ./services/taskapi ./tools/taskctl
go work sync

Le piege consiste a utiliser go.work comme astuce cachee de dependances. Le code peut passer localement parce que le workspace pointe vers des modules locaux, puis echouer en CI ou sur la machine d un collegue. Decidez si go.work est une infrastructure d equipe ou un etat local personnel, et demandez a Claude Code d expliquer tout nouveau module ou toute nouvelle dependance avant de les modifier.

Partager une couche service entre API et CLI

En Go, il est courant de placer les executables sous cmd/ et la logique metier reutilisable sous internal/. L exemple ci-dessous garde le Store central independant de HTTP, afin qu une CLI puisse plus tard utiliser le meme comportement. Il n utilise que la bibliotheque standard. Collez-le dans cmd/taskapi/main.go.

// cmd/taskapi/main.go
package main

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"log/slog"
	"net/http"
	"os"
	"os/signal"
	"strings"
	"sync"
	"time"
)

var ErrValidation = errors.New("validation failed")

type Task struct {
	ID        string    `json:"id"`
	Title     string    `json:"title"`
	Status    string    `json:"status"`
	CreatedAt time.Time `json:"createdAt"`
}

type Store struct {
	mu    sync.Mutex
	next  int
	tasks map[string]Task
}

func NewStore() *Store {
	return &Store{next: 1, tasks: make(map[string]Task)}
}

func (s *Store) Create(ctx context.Context, title string) (Task, error) {
	select {
	case <-ctx.Done():
		return Task{}, fmt.Errorf("create task canceled: %w", ctx.Err())
	default:
	}

	title = strings.TrimSpace(title)
	if title == "" {
		return Task{}, fmt.Errorf("%w: title is required", ErrValidation)
	}

	s.mu.Lock()
	defer s.mu.Unlock()

	task := Task{
		ID:        fmt.Sprintf("task-%06d", s.next),
		Title:     title,
		Status:    "open",
		CreatedAt: time.Now().UTC(),
	}
	s.next++
	s.tasks[task.ID] = task
	return task, nil
}

func (s *Store) List(ctx context.Context) ([]Task, error) {
	select {
	case <-ctx.Done():
		return nil, fmt.Errorf("list tasks canceled: %w", ctx.Err())
	default:
	}

	s.mu.Lock()
	defer s.mu.Unlock()

	tasks := make([]Task, 0, len(s.tasks))
	for _, task := range s.tasks {
		tasks = append(tasks, task)
	}
	return tasks, nil
}

func main() {
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
	defer stop()

	logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
	store := NewStore()
	mux := http.NewServeMux()

	mux.HandleFunc("GET /tasks", func(w http.ResponseWriter, r *http.Request) {
		tasks, err := store.List(r.Context())
		if err != nil {
			writeError(w, err)
			return
		}
		writeJSON(w, http.StatusOK, tasks)
	})

	mux.HandleFunc("POST /tasks", func(w http.ResponseWriter, r *http.Request) {
		var body struct {
			Title string `json:"title"`
		}
		if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
			return
		}
		task, err := store.Create(r.Context(), body.Title)
		if err != nil {
			writeError(w, err)
			return
		}
		writeJSON(w, http.StatusCreated, task)
	})

	server := &http.Server{
		Addr:              ":8080",
		Handler:           mux,
		ReadHeaderTimeout: 5 * time.Second,
	}

	go func() {
		logger.Info("taskapi listening", "addr", server.Addr)
		if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
			logger.Error("server failed", "error", err)
			os.Exit(1)
		}
	}()

	<-ctx.Done()
	shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	if err := server.Shutdown(shutdownCtx); err != nil {
		logger.Error("graceful shutdown failed", "error", err)
	}
}

func writeJSON(w http.ResponseWriter, status int, v any) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	_ = json.NewEncoder(w).Encode(v)
}

func writeError(w http.ResponseWriter, err error) {
	switch {
	case errors.Is(err, ErrValidation):
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
	case errors.Is(err, context.Canceled), errors.Is(err, context.DeadlineExceeded):
		writeJSON(w, http.StatusRequestTimeout, map[string]string{"error": err.Error()})
	default:
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal error"})
	}
}

Lancez-le ainsi :

go mod init example.com/taskapi
mkdir -p cmd/taskapi
go run ./cmd/taskapi
curl -s http://localhost:8080/tasks
curl -s -X POST http://localhost:8080/tasks \
  -H 'content-type: application/json' \
  -d '{"title":"write table-driven tests"}'

Si vous ajoutez ensuite une CLI, ne copiez pas la logique du handler. Appelez la meme couche service. Le sujet est detaille dans developper des CLI avec Claude Code.

Faire des table-driven tests une condition de fin

Une table-driven test liste des entrees et des resultats attendus dans une slice, puis execute la meme logique pour chaque ligne. C est un motif naturel en Go pour la validation, les erreurs et les cas limites. Demandez des cas precis, pas seulement “ecris des tests”.

// cmd/taskapi/store_test.go
package main

import (
	"context"
	"errors"
	"fmt"
	"testing"
)

func TestStoreCreate(t *testing.T) {
	tests := []struct {
		name    string
		title   string
		wantErr error
	}{
		{name: "valid title", title: "ship release notes"},
		{name: "blank title", title: "   ", wantErr: ErrValidation},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			store := NewStore()
			task, err := store.Create(context.Background(), tt.title)

			if tt.wantErr != nil {
				if !errors.Is(err, tt.wantErr) {
					t.Fatalf("error = %v, want %v", err, tt.wantErr)
				}
				return
			}

			if err != nil {
				t.Fatalf("unexpected error: %v", err)
			}
			if task.ID == "" || task.Status != "open" {
				t.Fatalf("unexpected task: %+v", task)
			}
		})
	}
}

func TestStoreCreateHonorsCanceledContext(t *testing.T) {
	store := NewStore()
	ctx, cancel := context.WithCancel(context.Background())
	cancel()

	_, err := store.Create(ctx, "will not be created")
	if !errors.Is(err, context.Canceled) {
		t.Fatalf("error = %v, want context.Canceled", err)
	}
}

func BenchmarkStoreCreate(b *testing.B) {
	store := NewStore()
	ctx := context.Background()
	b.ReportAllocs()

	for i := 0; i < b.N; i++ {
		if _, err := store.Create(ctx, fmt.Sprintf("task-%d", i)); err != nil {
			b.Fatal(err)
		}
	}
}

Ajoutez ces commandes aux criteres d acceptation :

gofmt -w cmd/taskapi
go test ./...
go test -race ./...
go test -run TestStoreCreate -count=1 ./...
go test -bench=. -benchmem ./...

Le package officiel testing documente tests et benchmarks. Un benchmark est une boucle de mesure pilotee par Go via b.N; il evite de conclure sur une simple impression de vitesse.

Preserver l annulation par context

context.Context transporte annulation, deadlines et valeurs de requete entre frontieres d API. Le package officiel context explique que les requetes serveur entrantes creent un Context et que les appels sortants doivent l accepter.

Une erreur frequente de Claude Code est de recevoir r.Context() dans le handler, puis de recreer context.Background() dans un service ou repository. Cela casse la chaine d annulation. Si le client se deconnecte, une requete base de donnees ou un appel externe peut continuer. Ecrivez dans le prompt : ne pas creer de nouveau background context dans les couches basses, transmettre le ctx de l appelant.

Autre erreur : oublier d appeler le cancel renvoye par context.WithTimeout. Demandez a Claude Code d utiliser defer cancel() et de lancer go vet quand il introduit des contexts derives.

Verifier la concurrence avec race detector

Les goroutines sont legeres, mais les donnees partagees doivent etre synchronisees. Une data race arrive quand des goroutines accedent simultanement a la meme variable, qu au moins un acces ecrit et qu il n y a pas de synchronisation. Le Data Race Detector officiel s execute avec go test -race. Il ne trouve que les races des chemins executes.

Evitez les ecritures concurrentes dans un map partage :

func CountByStatusBad(tasks []Task) map[string]int {
	counts := make(map[string]int)
	var wg sync.WaitGroup

	for _, task := range tasks {
		task := task
		wg.Add(1)
		go func() {
			defer wg.Done()
			counts[task.Status]++
		}()
	}

	wg.Wait()
	return counts
}

Protegez l etat partage avec un mutex :

func CountByStatus(tasks []Task) map[string]int {
	counts := make(map[string]int)
	var (
		mu sync.Mutex
		wg sync.WaitGroup
	)

	for _, task := range tasks {
		task := task
		wg.Add(1)
		go func() {
			defer wg.Done()
			mu.Lock()
			counts[task.Status]++
			mu.Unlock()
		}()
	}

	wg.Wait()
	return counts
}

Le prompt ne doit pas se limiter a “rends cela concurrent”. Demandez a Claude Code d identifier les variables partagees, la responsabilite de fermeture des channels, le comportement d annulation, la correction du WaitGroup et le resultat de go test -race. Pour la revue, reliez cela a la revue de code avec Claude Code.

Prompts surs pour Go

Un bon prompt Go definit le perimetre d edition, les contrats, les commandes de verification et les changements interdits :

Objectif: ajouter validation et tests pour POST /tasks dans taskapi.
Fichiers autorises:
- cmd/taskapi/main.go
- cmd/taskapi/store_test.go
Regles:
- Ne pas changer le module path de go.mod
- Ne pas ajouter de nouvelle dependance externe
- Ne pas renommer les types publics ni les champs JSON
- Propager context.Context du handler au service
- Wrapper les erreurs avec fmt.Errorf("%w") et les verifier avec errors.Is
Done signifie:
- gofmt -w cmd/taskapi
- go test ./...
- go test -race ./...
- Rapporter brievement le changement et le risque restant

Si une dependance semble necessaire, forcez une pause :

Si tu penses qu une bibliotheque externe est necessaire, explique le candidat, pourquoi elle est necessaire, pourquoi la bibliotheque standard ne suffit pas et l impact sur go.mod avant implementation. Ne lance pas go get sans approbation.

Cela evite l ajout surprise de frameworks CLI, routers, ORMs ou librairies de mock. Go va souvent loin avec la bibliotheque standard ; les dependances doivent donc etre revues.

Cas d usage et pieges

Premier cas : ajouter un petit endpoint a une API existante. Incluez handler, methode service, table-driven tests et go test -race dans une seule tache. Le piege est d ajouter seulement le handler et de casser la frontiere service ou le format d erreur.

Deuxieme cas : partager la logique entre CLI et API. Si l equipe operations utilise une CLI et que l interface admin appelle une API pour la meme regle, placez la regle dans internal/service. Le piege est de copier la logique dans la CLI puis de la laisser diverger.

Troisieme cas : paralleliser une aggregation lente. Un dashboard qui appelle trois APIs externes peut profiter des goroutines. Les pieges sont les ecritures dans slices ou maps partagees, la fermeture d un channel depuis plusieurs endroits et les fuites de goroutines apres annulation.

Quatrieme cas : ameliorer les performances. Avant de demander “rends cela plus rapide”, prenez une base avec go test -bench=. -benchmem. Le piege est d ajouter cache ou goroutines sans mesure, en augmentant memoire et complexite.

Cinquieme cas : travailler dans un depot multi-module. go.work facilite l edition locale, mais il faut confirmer go env GOWORK et lancer les tests depuis le meme endroit que CI. Le piege est de prendre un succes local du workspace pour une preuve de production.

CTA et prochaine etape

Les lecteurs qui utilisent Claude Code avec Go cherchent souvent une adoption sure dans un vrai depot d equipe. Pour un workflow personnel rapide, commencez par la fiche gratuite. Pour regrouper CLAUDE.md, permissions, prompts de revue et checklists Go, consultez produits et modeles. Pour une adoption d equipe sur APIs Go, CLIs, CI et regles de revue, utilisez formation et conseil Claude Code.

Resume

Le workflow Go stable est le suivant : cartographier le depot, inspecter go.mod et go.work, garder les changements API et CLI derriere une couche service, integrer les table-driven tests a la definition de done, preserver context cancellation, lancer race detector et mesurer avant de promettre des gains.

J ai teste ce flux sur une petite API de taches. Le changement le plus utile a ete d inclure directement les commandes de verification dans le prompt. Quand gofmt, go test ./..., go test -race ./... et go test -bench=. -benchmem sont devenus des criteres de fin, le rapport de Claude Code est passe de “implemente” a “verifie, avec ces risques restants”. Avant publication, verifiez aussi les liens officiels, liens internes, profondeur du texte, code fences, updatedDate et heroImage.

#Claude Code #Go #Golang #go.mod #Tests #Concurrence
Gratuit

PDF gratuit: cheatsheet Claude Code

Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.

Nous protégeons vos données et n'envoyons pas de spam.

Masa

À propos de l'auteur

Masa

Ingénieur spécialisé dans les workflows pratiques avec Claude Code.