Desenvolvimento Go com Claude Code: go.mod, testes, concorrencia e race detector
Guia pratico Claude Code para Go: mapa do repo, go.mod, go work, API/CLI, testes, context, concorrencia, benchmarks.
Ao usar Claude Code para desenvolvimento Go, o prompt arriscado e “crie uma API” ou “adicione testes” sem contexto do repositorio. O codigo pode rodar, mas pode fugir do go.mod, dos limites de pacote, da politica de erros, da propagacao de context e da seguranca de concorrencia. Go parece simples, entao esses problemas costumam ficar escondidos em diffs pequenos.
Este guia trata Claude Code como um agente de desenvolvimento Go, nao apenas um gerador de codigo. O fluxo cobre mapa do repositorio, decisoes de modulo e workspace, mudancas em API e CLI, table-driven tests, wrapping de erros, cancelamento com context, armadilhas de concorrencia, race detector, benchmarks e prompts seguros. Um modulo e a unidade que gerencia dependencias, um workspace e a base para trabalhar com varios modulos locais, e context transmite cancelamento e deadlines para chamadas inferiores.
Masa testou esse fluxo em uma pequena API de tarefas em Go. O primeiro prompt dizia apenas “adicione um handler”; funcionou localmente, mas nao tinha testes, o cancelamento era ambiguo e havia risco de escrever em um map compartilhado a partir de goroutines. Ao mudar para “leia o repo e verifique com go test -race”, a revisao ficou muito mais objetiva.
Primeiro faca um mapa do repositorio
A primeira tarefa em Go nao e gerar codigo. E criar um mapa: quais diretorios contem comandos, quais pacotes sao internos, qual go.mod controla dependencias e o que o CI ja verifica. Peca a Claude Code uma primeira passada somente leitura.
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 ./...
Use um prompt limitado:
Inspecione este repositorio Go. Ainda nao edite arquivos.
Reporte:
- Se go.mod e go.work existem
- O papel de cmd, internal, pkg, api, migrations e testdata
- Tipos e funcoes publicas que podem quebrar compatibilidade se mudarem
- O estilo atual de tratamento de erros
- Fronteiras que recebem context.Context
- O resultado de go test ./...
- O menor conjunto de arquivos seguro para a proxima tarefa
Como referencias oficiais, use Organizing a Go module para layout e Go Modules Reference para comportamento de modulos. Para Claude Code, veja 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"]
Fluxos relacionados aparecem em mapa de codigo existente e boas praticas de CLAUDE.md. Em Go, a regra e a mesma: fixe o acordo de trabalho antes de pedir implementacao.
Nao deixe go.mod e go work derivarem
go.mod registra o caminho do modulo, a versao de Go e as dependencias. Uma aplicacao pequena normalmente precisa de um unico go.mod. Um repositorio multi-modulo pode usar go.work para que o comando go enxergue varios modulos locais ao mesmo tempo. O tutorial oficial e Getting started with multi-module workspaces.
Antes de permitir que Claude Code adicione dependencias, inspecione o estado atual:
go env GOMOD GOWORK
go list -m all
go mod tidy
go test ./...
Crie um workspace apenas quando realmente precisar 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
A armadilha e usar go.work como truque oculto de dependencias. O codigo pode passar localmente porque o workspace aponta para modulos locais, mas falhar no CI ou na maquina de outra pessoa. Decida se go.work e infraestrutura do time ou estado local pessoal, e peca a Claude Code que explique qualquer novo modulo ou dependencia antes de alterar.
Compartilhe uma camada service entre API e CLI
Em Go, e comum colocar executaveis em cmd/ e logica reutilizavel em internal/. O exemplo abaixo mantem o Store central separado do HTTP para que uma CLI possa reutilizar o mesmo comportamento depois. Ele usa apenas a biblioteca padrao. Cole em 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"})
}
}
Execute assim:
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"}'
Se adicionar uma CLI depois, nao copie a logica do handler. Chame a mesma camada service. Para isso, veja desenvolvimento de CLI com Claude Code.
Coloque table-driven tests na definicao de pronto
Uma table-driven test lista entradas e resultados esperados em uma slice e executa a mesma logica para cada linha. E um padrao natural em Go para validacao, erros e casos de borda. Peca casos concretos, nao apenas “escreva testes”.
// 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)
}
}
}
Inclua estes comandos nos criterios de aceite:
gofmt -w cmd/taskapi
go test ./...
go test -race ./...
go test -run TestStoreCreate -count=1 ./...
go test -bench=. -benchmem ./...
O pacote oficial testing documenta testes e benchmarks. Um benchmark e um loop de medicao controlado pelo Go por meio de b.N; ele evita decisoes baseadas em sensacao.
Preserve context cancellation
context.Context carrega cancelamento, deadlines e valores de request entre fronteiras de API. O pacote oficial context explica que requests de servidor criam um Context e chamadas de saida devem aceitar um.
Um erro comum de Claude Code e receber r.Context() no handler e depois criar context.Background() dentro do service ou repository. Isso quebra a cadeia de cancelamento. Se o cliente desconectar, a consulta ao banco ou chamada externa pode continuar. Escreva no prompt: nao criar novo background context em camadas inferiores; passar o ctx do chamador.
Outro erro e esquecer de chamar o cancel retornado por context.WithTimeout. Peca a Claude Code para usar defer cancel() e executar go vet quando introduzir contexts derivados.
Verifique concorrencia com race detector
Goroutines sao leves, mas dados compartilhados ainda precisam de sincronizacao. Uma data race ocorre quando goroutines acessam a mesma variavel ao mesmo tempo, pelo menos um acesso escreve e nao ha sincronizacao. O Data Race Detector oficial roda com go test -race. Ele so encontra races em caminhos executados.
Evite escritas concorrentes em um map compartilhado:
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
}
Proteja o estado compartilhado com 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
}
O prompt nao deve parar em “torne isso concorrente”. Peca a Claude Code para identificar variaveis compartilhadas, responsabilidade de fechar channels, comportamento de cancelamento, correcao de WaitGroup e resultado de go test -race. Para revisao, conecte com code review usando Claude Code.
Prompts seguros para Go
Bons prompts Go definem escopo de edicao, contratos, comandos de verificacao e mudancas proibidas:
Objetivo: adicionar validacao e testes para POST /tasks no taskapi.
Arquivos permitidos:
- cmd/taskapi/main.go
- cmd/taskapi/store_test.go
Regras:
- Nao mudar o module path de go.mod
- Nao adicionar novas dependencias externas
- Nao renomear tipos publicos nem campos JSON
- Propagar context.Context do handler ate o service
- Envolver erros com fmt.Errorf("%w") e verificar com errors.Is
Done significa:
- gofmt -w cmd/taskapi
- go test ./...
- go test -race ./...
- Reportar brevemente a mudanca e o risco restante
Se uma dependencia parecer necessaria, force uma pausa:
Se voce acredita que uma biblioteca externa e necessaria, explique a candidata, por que ela e necessaria, por que a biblioteca padrao nao basta e o impacto no go.mod antes da implementacao. Nao execute go get sem aprovacao.
Isso reduz adicoes surpresa de frameworks CLI, routers, ORMs ou bibliotecas de mock. Go frequentemente vai longe com a biblioteca padrao, entao mudancas de dependencia merecem revisao.
Casos de uso e armadilhas
O primeiro caso de uso e adicionar um endpoint pequeno a uma API existente. Inclua handler, metodo de service, table-driven tests e go test -race em uma unica tarefa. A armadilha e adicionar apenas o handler e quebrar a fronteira do service ou o formato de erro.
O segundo e compartilhar logica entre CLI e API. Se operacoes usa uma CLI e o painel admin chama uma API para a mesma regra, mova a regra para internal/service. A armadilha e copiar logica para a CLI e deixar as duas versoes divergirem.
O terceiro e paralelizar agregacao lenta. Um dashboard que chama tres APIs externas pode se beneficiar de goroutines. As armadilhas sao escritas em slices/maps compartilhados, varios pontos fechando o mesmo channel e vazamento de goroutines apos cancelamento.
O quarto e melhoria de performance. Antes de pedir “deixe mais rapido”, colete uma linha de base com go test -bench=. -benchmem. A armadilha e adicionar cache ou goroutines sem medir, aumentando memoria e complexidade.
O quinto e trabalho em repos multi-modulo. go.work facilita edicoes locais, mas voce deve confirmar go env GOWORK e rodar testes do mesmo lugar que o CI. A armadilha e tratar um sucesso local do workspace como evidencia de producao.
CTA e proximo passo
Leitores que usam Claude Code em Go normalmente querem adocao segura em um repositorio real de equipe. Para um fluxo pessoal rapido, comece com o cheatsheet gratuito. Para reunir CLAUDE.md, permissoes, prompts de revisao e checklists Go, veja produtos e templates. Para adocao de equipe em APIs Go, CLIs, CI e regras de revisao, use treinamento e consultoria Claude Code.
Resumo
O fluxo Go estavel e: mapear o repositorio primeiro, inspecionar go.mod e go.work, manter mudancas de API/CLI atras de uma camada service, incluir table-driven tests na definicao de pronto, preservar context cancellation, rodar race detector e medir antes de prometer ganhos de performance.
Testei esse fluxo em uma pequena API de tarefas. A mudanca de maior impacto foi colocar os comandos de verificacao diretamente no prompt. Quando gofmt, go test ./..., go test -race ./... e go test -bench=. -benchmem viraram criterios de conclusao, o relatorio de Claude Code mudou de “implementado” para “verificado, com estes riscos restantes”. Antes de publicar, verifique tambem links oficiais, links internos, profundidade do texto, code fences, updatedDate e heroImage.
PDF grátis: cheatsheet do Claude Code
Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.
Cuidamos dos seus dados e não enviamos spam.
Sobre o autor
Masa
Engenheiro focado em workflows práticos com Claude Code.
Artigos relacionados
Workflow Obsidian para CLAUDE.md com Claude Code
Transforme notas de trabalho do Obsidian em notas operacionais CLAUDE.md para preservar contexto.
Claude Code Revenue CTA Routing: artigos para PDF, Gumroad e consultoria
Um fluxo com Claude Code para levar leitores ao PDF grátis, Gumroad ou consultoria conforme intenção.
Regras de handoff para equipes com Claude Code: evidências, permissões, rollback e receita
Formato prático para entregar trabalho do Claude Code com prova, permissões, rollback, PDF grátis, Gumroad e consultoria.