Go-Entwicklung mit Claude Code: go.mod, Tests, Nebenlaeufigkeit und Race Checks
Praxisguide fuer Claude Code in Go: Repo-Map, go.mod, go work, API/CLI, Tests, context, Concurrency, Benchmarks.
Wenn du Claude Code fuer Go-Entwicklung nutzt, ist der riskante Prompt “baue eine API” oder “fuege Tests hinzu” ohne Repository-Kontext. Du bekommst vielleicht Code, der lokal laeuft, aber er kann vom bestehenden go.mod, von Paketgrenzen, Fehlerkonventionen, context-Weitergabe und Nebenlaeufigkeitssicherheit abweichen. Go wirkt einfach, deshalb verstecken sich solche Fehler oft in kleinen Diffs.
Dieser Leitfaden behandelt Claude Code als Go-Entwicklungsagenten, nicht nur als Codegenerator. Der Ablauf umfasst Repository-Mapping, Modul- und Workspace-Entscheidungen, API- und CLI-Aenderungen, table-driven tests, Error Wrapping, context cancellation, Nebenlaeufigkeitsfallen, Race Detector, Benchmarks und sichere Prompts. Ein Modul ist die Einheit fuer Abhaengigkeiten, ein Workspace ist ein Arbeitsrahmen fuer mehrere lokale Module, und context traegt Abbruchsignale und Deadlines in tiefere Aufrufe.
Masa hat diesen Ablauf an einer kleinen Go-Task-API getestet. Der erste Prompt lautete nur “fuege einen Handler hinzu”; lokal funktionierte es, aber Tests fehlten, Abbruchverhalten war unklar und eine goroutine schrieb riskant in eine geteilte map. Nach dem Wechsel zu “lies zuerst das Repo und verifiziere mit go test -race” wurde die Review deutlich konkreter.
Zuerst eine Repository-Map erstellen
Die erste Go-Aufgabe ist nicht Codegenerierung. Es ist eine Karte: Welche Verzeichnisse enthalten Commands, welche Packages sind intern, welches go.mod steuert Abhaengigkeiten und was prueft CI bereits. Bitte Claude Code zuerst um eine reine Leseanalyse.
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 ./...
Ein fokussierter Prompt sieht so aus:
Untersuche dieses Go-Repository. Aendere noch keine Dateien.
Berichte:
- Ob go.mod und go.work existieren
- Den Zweck von cmd, internal, pkg, api, migrations und testdata
- Public types/functions, deren Aenderung Kompatibilitaet brechen koennte
- Den vorhandenen Stil fuer Fehlerbehandlung
- Grenzen, die context.Context akzeptieren
- Das Ergebnis von go test ./...
- Die kleinste sichere Dateimenge fuer die naechste Aufgabe
Als offizielle Referenzen dienen Organizing a Go module fuer Struktur und Go Modules Reference fuer Modulverhalten. Fuer Claude Code selbst nutze Claude Code overview.
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"]
Verwandte Workflows findest du unter Repo-Mapping fuer bestehende Codebases und CLAUDE.md Best Practices. Fuer Go gilt dieselbe Regel: Erst Arbeitsvertrag fixieren, dann Implementierung anfordern.
go.mod und go work nicht driften lassen
go.mod speichert Modulpfad, Go-Version und Abhaengigkeiten. Eine kleine Anwendung braucht oft nur ein go.mod. Ein Multi-Modul-Repository kann go.work nutzen, damit der go-Befehl mehrere lokale Module gleichzeitig sieht. Das offizielle Tutorial ist Getting started with multi-module workspaces.
Bevor Claude Code Abhaengigkeiten hinzufuegt, pruefe den aktuellen Zustand:
go env GOMOD GOWORK
go list -m all
go mod tidy
go test ./...
Erstelle einen Workspace nur, wenn du wirklich mehrere Module zusammen bearbeiten musst:
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
Die Falle ist, go.work als versteckten Abhaengigkeitstrick zu verwenden. Lokal kann alles bestehen, weil der Workspace auf lokale Module zeigt, waehrend CI oder andere Maschinen scheitern. Entscheide, ob go.work Team-Infrastruktur oder persoenlicher lokaler Zustand ist, und verlange von Claude Code eine Begruendung fuer jedes neue Modul und jede neue Abhaengigkeit.
API und CLI ueber dieselbe Service-Schicht fuehren
In Go liegen Executables oft unter cmd/, wiederverwendbare Fachlogik unter internal/. Das folgende Beispiel trennt den zentralen Store von HTTP, damit spaeter auch eine CLI dasselbe Verhalten nutzen kann. Es verwendet nur die Standardbibliothek. Fuege es in cmd/taskapi/main.go ein.
// 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"})
}
}
Starte es so:
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"}'
Wenn du spaeter eine CLI ergaenzt, kopiere nicht die Handler-Logik. Rufe dieselbe Service-Schicht auf. Siehe dazu CLI-Tools mit Claude Code entwickeln.
Table-driven Tests als Definition of Done
Ein table-driven test listet Eingaben und erwartete Ergebnisse in einer Slice und fuehrt dieselbe Testlogik fuer jede Zeile aus. Das passt gut zu Validierung, Fehlern und Grenzfaellen. Bitte Claude Code um konkrete Faelle, nicht nur um “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)
}
}
}
Diese Befehle gehoeren in die Akzeptanzkriterien:
gofmt -w cmd/taskapi
go test ./...
go test -race ./...
go test -run TestStoreCreate -count=1 ./...
go test -bench=. -benchmem ./...
Das offizielle testing package dokumentiert Tests und Benchmarks. Ein Benchmark ist eine von Go ueber b.N gesteuerte Messschleife und verhindert Entscheidungen nach Gefuehl.
Context Cancellation bewahren
context.Context transportiert Abbruch, Deadlines und request-bezogene Werte ueber API-Grenzen. Das offizielle context package beschreibt, dass eingehende Server-Requests einen Context erzeugen und ausgehende Aufrufe einen Context akzeptieren sollten.
Ein haeufiger Claude-Code-Fehler ist: Der Handler bekommt r.Context(), aber Service oder Repository erstellen erneut context.Background(). Damit bricht die Abbruchkette. Wenn der Client trennt, koennen Datenbankabfragen oder externe API-Aufrufe weiterlaufen. Schreibe in den Prompt: In tieferen Schichten keinen neuen background context erstellen, sondern den ctx des Aufrufers weitergeben.
Ein zweiter Fehler ist, den von context.WithTimeout gelieferten cancel nicht aufzurufen. Lass Claude Code defer cancel() setzen und bei neuen abgeleiteten Contexts go vet ausfuehren.
Nebenlaeufigkeit mit Race Detector pruefen
Goroutines sind leichtgewichtig, aber geteilte Daten brauchen Synchronisation. Eine data race entsteht, wenn Goroutines gleichzeitig auf dieselbe Variable zugreifen, mindestens ein Zugriff schreibt und keine Synchronisation existiert. Der offizielle Data Race Detector laeuft mit go test -race. Er findet nur Races in ausgefuehrten Codepfaden.
Vermeide gleichzeitige Schreibzugriffe auf eine geteilte map:
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
}
Schuetze geteilten Zustand mit einem 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
}
Der Prompt sollte nicht nur “mach es nebenlaeufig” sagen. Claude Code soll geteilte Variablen, Channel-Close-Verantwortung, Cancellation-Verhalten, WaitGroup-Korrektheit und das Ergebnis von go test -race pruefen. Fuer Review-Fragen siehe Code Review mit Claude Code.
Sichere Claude-Code-Prompts fuer Go
Gute Go-Prompts definieren Editierumfang, Vertrage, Verifikationsbefehle und verbotene Aenderungen:
Ziel: Validierung und Tests fuer POST /tasks in taskapi hinzufuegen.
Erlaubte Dateien:
- cmd/taskapi/main.go
- cmd/taskapi/store_test.go
Regeln:
- go.mod module path nicht aendern
- Keine neuen externen Abhaengigkeiten hinzufuegen
- Public types und JSON fields nicht umbenennen
- context.Context vom Handler bis zum Service weitergeben
- Fehler mit fmt.Errorf("%w") wrappen und mit errors.Is pruefen
Done bedeutet:
- gofmt -w cmd/taskapi
- go test ./...
- go test -race ./...
- Aenderung und Restrisiko kurz berichten
Falls eine Abhaengigkeit noetig wirken koennte, erzwinge eine Pause:
Wenn du eine externe Bibliothek fuer notwendig haeltst, erklaere vor der Implementierung Kandidat, Grund, warum die Standardbibliothek nicht reicht, und Auswirkungen auf go.mod. Fuehre go get nicht ohne Freigabe aus.
So vermeidest du ueberraschende CLI-Frameworks, Router, ORMs oder Mocking-Bibliotheken. Go kommt oft weit mit der Standardbibliothek, daher gehoeren Abhaengigkeitsaenderungen immer in die Review.
Praxisfaelle und Fallen
Der erste Praxisfall ist ein kleiner Endpoint in einer bestehenden API. Handler, Service-Methode, table-driven tests und go test -race gehoeren in eine Aufgabe. Die Falle ist, nur den Handler zu ergaenzen und Service-Grenze oder Fehlerformat zu brechen.
Der zweite Fall ist gemeinsame Logik fuer CLI und API. Wenn Operations eine CLI nutzt und das Admin-UI dieselbe Regel per API ausfuehrt, gehoert die Regel nach internal/service. Die Falle ist kopierte CLI-Logik, die spaeter driftet.
Der dritte Fall ist Parallelisierung langsamer Aggregation. Ein Dashboard, das drei externe APIs aufruft, kann von goroutines profitieren. Fallen sind Schreibzugriffe auf geteilte slices/maps, mehrere Stellen, die denselben channel schliessen, und goroutine leaks nach Cancellation.
Der vierte Fall ist Performance-Arbeit. Vor “mach es schneller” steht eine Basis mit go test -bench=. -benchmem. Die Falle ist Cache oder goroutines ohne Messung hinzuzufuegen und nur Speicherverbrauch und Komplexitaet zu erhoehen.
Der fuenfte Fall ist Arbeit in Multi-Modul-Repositories. go.work erleichtert lokale Edits, aber go env GOWORK und der Testort muessen zu CI passen. Die Falle ist, einen lokalen Workspace-Erfolg als Produktionsnachweis zu behandeln.
CTA und naechster Schritt
Leser, die Claude Code fuer Go nutzen, wollen meist sichere Einfuehrung in einem echten Team-Repository. Fuer einen schnellen persoenlichen Workflow starte mit dem kostenlosen Cheatsheet. Fuer CLAUDE.md, Berechtigungen, Review-Prompts und Go-Checklisten in einem Paket siehe Produkte und Templates. Fuer Team-Einfuehrung ueber Go APIs, CLIs, CI und Review-Regeln nutze Claude Code Training und Beratung.
Zusammenfassung
Der stabile Go-Workflow lautet: zuerst Repository mappen, go.mod und go.work pruefen, API- und CLI-Aenderungen hinter einer Service-Schicht halten, table-driven tests zur Definition of Done machen, context cancellation bewahren, Race Detector ausfuehren und vor Performance-Versprechen messen.
Ich habe diesen Ablauf an einer kleinen Task-API getestet. Die wirkungsvollste Aenderung war, Verifikationsbefehle direkt in den Prompt zu schreiben. Als gofmt, go test ./..., go test -race ./... und go test -bench=. -benchmem Abschlusskriterien wurden, wechselte Claude Codes Bericht von “implementiert” zu “verifiziert, mit diesen Restrisiken”. Vor der Veroeffentlichung pruefe ausserdem offizielle Links, interne Links, Texttiefe, Code-Fences, updatedDate und heroImage.
Kostenloses PDF: Claude-Code-Cheatsheet
E-Mail eintragen und eine Seite mit Befehlen, Review-Gewohnheiten und sicheren Workflows herunterladen.
Wir schützen Ihre Daten und senden keinen Spam.
Über den Autor
Masa
Engineer für praktische Claude-Code-Workflows und Team-Einführung.
Ähnliche Artikel
Claude Code Workflow von Obsidian zu CLAUDE.md
Obsidian-Arbeitsnotizen in CLAUDE.md-Betriebsnotizen verwandeln und Kontext nicht ständig neu erklären.
Claude Code Revenue CTA Routing: Artikel zu PDF, Gumroad und Beratung führen
Ein Claude-Code-Ablauf, der Leser nach Absicht zu Gratis-PDF, Gumroad oder Beratung führt.
Claude-Code-Team-Handoff-Regeln: Belege, Berechtigungen, Rollback und Umsatzpfade
Ein praktisches Claude-Code-Handoff für Review-Belege, Berechtigungen, Rollback, Gratis-PDF, Gumroad und Beratung.