Use Cases (Actualizado: 2/6/2026)

Desarrollo Go con Claude Code: go.mod, pruebas, concurrencia y race detector

Guia practica para usar Claude Code en Go: mapa del repo, go.mod, go work, API/CLI, pruebas, context y concurrencia.

Desarrollo Go con Claude Code: go.mod, pruebas, concurrencia y race detector

Cuando usas Claude Code para desarrollo Go, el prompt peligroso es “crea una API” o “agrega tests” sin contexto del repositorio. Puede producir codigo que corre, pero que no respeta el go.mod, los limites de paquetes, la politica de errores, la propagacion de context o la seguridad de concurrencia. Go parece simple, y por eso estos problemas suelen esconderse en diffs pequenos.

Esta guia trata a Claude Code como un agente de desarrollo Go, no solo como un generador de codigo. El flujo cubre mapa del repositorio, decisiones de modulo y workspace, cambios de API y CLI, table-driven tests, wrapping de errores, cancelacion con context, trampas de concurrencia, race detector, benchmarks y prompts seguros. Un modulo es la unidad que gestiona dependencias, un workspace es una base para trabajar con varios modulos locales, y context es la forma habitual de pasar cancelacion y deadlines hacia llamadas inferiores.

Masa probo este flujo en una pequena API de tareas escrita en Go. El primer prompt decia solo “agrega un handler”; funciono localmente, pero no tenia tests, la cancelacion era ambigua y habia riesgo de escribir un map compartido desde goroutines. Al cambiar el prompt a “lee el repo y verifica con go test -race”, la revision se volvio mucho mas concreta.

Primero crea un mapa del repositorio

La primera tarea en Go no es generar codigo. Es crear un mapa: que directorios son comandos, que paquetes son internos, que go.mod controla dependencias y que verifica CI. Pide a Claude Code una primera pasada de solo lectura.

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 ./...

Usa un prompt acotado:

Inspecciona este repositorio Go. No edites archivos todavia.
Informa:
- Si existen go.mod y go.work
- El proposito de cmd, internal, pkg, api, migrations y testdata
- Tipos y funciones publicas que podrian romper compatibilidad si cambian
- El estilo actual de manejo de errores
- Limites que reciben context.Context
- El resultado de go test ./...
- El conjunto minimo de archivos que seria seguro tocar en la proxima tarea

Como referencias oficiales, usa Organizing a Go module para estructura y Go Modules Reference para comportamiento de modulos. Para Claude Code, revisa 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"]

Para flujos cercanos, consulta mapeo de repos existentes y buenas practicas de CLAUDE.md. En Go aplica la misma regla: fija el acuerdo de trabajo antes de pedir implementacion.

No dejes que go.mod y go work deriven

go.mod registra la ruta del modulo, la version de Go y las dependencias. Una app pequena suele necesitar un solo go.mod. Un repositorio con varios modulos puede usar go.work para que el comando go vea varios modulos locales a la vez. El tutorial oficial es Getting started with multi-module workspaces.

Antes de permitir que Claude Code agregue dependencias, inspecciona el estado actual:

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

Crea un workspace solo si de verdad necesitas editar varios modulos juntos:

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

La trampa es usar go.work como atajo oculto de dependencias. El codigo puede pasar localmente porque el workspace apunta a modulos locales, pero fallar en CI o en otra maquina. Decide si go.work es infraestructura del equipo o estado local personal, y pide a Claude Code que explique cualquier modulo o dependencia nueva antes de cambiarla.

Comparte una capa service entre API y CLI

En Go es comun poner ejecutables bajo cmd/ y logica reutilizable bajo internal/. El ejemplo siguiente mantiene el Store central separado de HTTP para que luego una CLI pueda usar el mismo comportamiento. Solo usa la biblioteca estandar. Pegalo en 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"})
	}
}

Ejecutalo asi:

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 agregas una CLI despues, no copies la logica del handler. Llama a la misma capa service. Para ese enfoque, lee desarrollo de CLI con Claude Code.

Haz que las table-driven tests sean parte de Done

Una table-driven test lista entradas y resultados esperados en una slice y ejecuta la misma logica por cada fila. Es un patron natural de Go para validacion, errores y bordes. Pide casos concretos, no solo “escribe 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)
		}
	}
}

Incluye estos comandos en los criterios de aceptacion:

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

El paquete oficial testing documenta tests y benchmarks. Un benchmark es un bucle de medicion controlado por Go mediante b.N; evita decidir por sensacion.

Conserva la cancelacion con context

context.Context transporta cancelacion, deadlines y valores de request entre limites de API. El paquete oficial context explica que las requests entrantes crean un Context y las llamadas salientes deben aceptarlo.

Un error comun de Claude Code es recibir r.Context() en el handler y luego crear context.Background() dentro del service o repository. Eso rompe la cancelacion. Si el cliente se desconecta, una consulta de base de datos o llamada externa puede seguir corriendo. Escribe en el prompt: no crear un nuevo background context en capas inferiores; pasar el ctx del llamador.

Otro error es olvidar llamar al cancel devuelto por context.WithTimeout. Pide a Claude Code que use defer cancel() y que ejecute go vet cuando introduzca contexts derivados.

Verifica concurrencia con race detector

Las goroutines son ligeras, pero los datos compartidos necesitan sincronizacion. Una data race ocurre cuando goroutines acceden a la misma variable de forma concurrente, al menos un acceso escribe y no hay sincronizacion. El Data Race Detector oficial corre con go test -race. Solo encuentra races en rutas ejecutadas.

Evita escrituras concurrentes a un map compartido:

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
}

Protege el estado compartido con 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
}

El prompt no debe quedarse en “hazlo concurrente”. Pide a Claude Code que identifique variables compartidas, responsabilidad de cerrar channels, comportamiento de cancelacion, correccion de WaitGroup y resultado de go test -race. Para revisar, conecta esto con revision de codigo con Claude Code.

Prompts seguros para Go

Un buen prompt Go define alcance de edicion, contratos, comandos de verificacion y cambios prohibidos:

Objetivo: agregar validacion y pruebas para POST /tasks en taskapi.
Archivos permitidos:
- cmd/taskapi/main.go
- cmd/taskapi/store_test.go
Reglas:
- No cambiar el module path de go.mod
- No agregar dependencias externas nuevas
- No renombrar tipos publicos ni campos JSON
- Propagar context.Context desde handler hasta service
- Envolver errores con fmt.Errorf("%w") y comprobarlos con errors.Is
Done significa:
- gofmt -w cmd/taskapi
- go test ./...
- go test -race ./...
- Reportar el cambio y el riesgo restante brevemente

Si puede hacer falta una dependencia, fuerza una pausa:

Si crees que una libreria externa es necesaria, explica el candidato, por que hace falta, por que la biblioteca estandar no basta y el impacto en go.mod antes de implementar. No ejecutes go get sin aprobacion.

Esto reduce incorporaciones sorpresa de frameworks CLI, routers, ORMs o librerias de mock. Go suele llegar lejos con la biblioteca estandar, asi que los cambios de dependencias merecen revision.

Casos de uso y trampas

El primer caso de uso es agregar un endpoint pequeno a una API existente. Incluye handler, metodo de service, table-driven tests y go test -race en una sola tarea. La trampa es agregar solo el handler y romper el limite de service o el formato de error.

El segundo caso es compartir logica entre CLI y API. Si operaciones usa una CLI y el panel admin llama a una API para la misma regla, mueve la regla a internal/service. La trampa es copiar logica para la CLI y dejar que derive.

El tercero es paralelizar agregaciones lentas. Un dashboard que llama tres APIs externas puede beneficiarse de goroutines. Las trampas son escribir en slices o maps compartidos, cerrar un channel desde varios lugares y filtrar goroutines tras una cancelacion.

El cuarto es optimizacion de rendimiento. Antes de pedir “hazlo mas rapido”, toma una linea base con go test -bench=. -benchmem. La trampa es agregar cache o goroutines sin medir, aumentando memoria y complejidad.

El quinto es trabajar en repos multi-modulo. go.work facilita ediciones locales, pero debes confirmar go env GOWORK y ejecutar tests desde el mismo lugar que CI. La trampa es tratar un pase local de workspace como evidencia de produccion.

CTA y siguiente paso

Quien usa Claude Code para Go suele querer adopcion segura en un repositorio real de equipo. Para un flujo personal rapido, empieza con la chuleta gratuita. Para empaquetar CLAUDE.md, permisos, prompts de revision y checklists de Go, revisa productos y plantillas. Para adopcion de equipo en APIs Go, CLIs, CI y reglas de revision, usa formacion y consultoria de Claude Code.

Resumen

El flujo Go estable es: mapear el repositorio primero, inspeccionar go.mod y go.work, mantener API y CLI detras de una capa service, hacer que las table-driven tests sean parte de Done, preservar context cancellation, correr race detector y medir antes de prometer rendimiento.

Probe este flujo en una pequena API de tareas. El cambio mas util fue incluir los comandos de verificacion directamente en el prompt. Cuando gofmt, go test ./..., go test -race ./... y go test -bench=. -benchmem se volvieron criterios de finalizacion, el reporte de Claude Code cambio de “implementado” a “verificado, con estos riesgos restantes”. Antes de publicar, revisa enlaces oficiales, enlaces internos, profundidad del texto, code fences, updatedDate y heroImage.

#Claude Code #Go #Golang #go.mod #Pruebas #Concurrencia
Gratis

PDF gratis: cheatsheet de Claude Code

Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.

Cuidamos tus datos y no enviamos spam.

Masa

Sobre el autor

Masa

Ingeniero enfocado en workflows prácticos con Claude Code.