Go has strong conventions around error handling, package structure, and concurrency that take time to internalize. Claude Code generates idiomatic Go — not just Go that compiles, but Go that passes go vet, uses proper error wrapping, and applies channels and goroutines correctly rather than cargo-culting patterns from other languages.
This guide covers using Claude Code for Go development: error handling patterns, interfaces, concurrency, testing, and building HTTP services.
Setting Up Claude Code for Go Projects
Your CLAUDE.md matters especially for Go because the community has strong opinions that aren’t obvious defaults:
# Go Project Context
## Stack
- Go 1.23, built with modules
- HTTP: standard library (net/http) + chi router
- Database: PostgreSQL via pgx v5 (not database/sql)
- Testing: standard library (testing package) + testify for assertions
## Conventions
- Error handling: wrap with fmt.Errorf("context: %w", err) — not fmt.Errorf("context: %v", err)
- Use %w for wrappable errors, %v for display only
- Interfaces: define where used (consumer side), not where implemented
- Table-driven tests for functions with multiple cases
- No global state — dependency injection via struct fields
## Never
- panic() in library code — only in main() for unrecoverable startup failures
- Naked returns (return without values in named return functions)
- Ignoring errors with _
- Using sync.Mutex when a sync.RWMutex would be more appropriate
See the CLAUDE.md guide for the full setup.
Error Handling Patterns
Go’s explicit error handling is one of its best features and one of its most frequently misused. Claude Code generates correct patterns:
Wrapping Errors
Write a function that reads a user from the database and wraps errors
with context. Use the %w verb so errors are unwrappable.
func (s *UserService) GetUser(ctx context.Context, id int64) (*User, error) {
user, err := s.repo.FindByID(ctx, id)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, fmt.Errorf("user %d not found: %w", id, ErrNotFound)
}
return nil, fmt.Errorf("fetching user %d: %w", id, err)
}
return user, nil
}
Claude uses %w for error wrapping (preserving the error chain for errors.Is and errors.As) and adds context about the operation that failed. This is idiomatic Go — the error message at the call site reads as a sentence describing what went wrong.
Custom Error Types
Create an error type for validation errors that includes
which fields failed and why. It should work with errors.As().
type ValidationError struct {
Fields map[string]string // field name → error message
}
func (e *ValidationError) Error() string {
if len(e.Fields) == 0 {
return "validation failed"
}
parts := make([]string, 0, len(e.Fields))
for field, msg := range e.Fields {
parts = append(parts, fmt.Sprintf("%s: %s", field, msg))
}
sort.Strings(parts)
return "validation failed: " + strings.Join(parts, ", ")
}
// Usage: errors.As(err, &validationErr) gives you the fields map
Error Handling in HTTP Handlers
Write an HTTP handler that returns proper status codes for different error types.
Map: ErrNotFound → 404, ValidationError → 422, everything else → 500.
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
if err != nil {
http.Error(w, "invalid user ID", http.StatusBadRequest)
return
}
user, err := h.service.GetUser(r.Context(), id)
if err != nil {
switch {
case errors.Is(err, ErrNotFound):
http.Error(w, "not found", http.StatusNotFound)
case errors.As(err, &ValidationError{}):
var ve *ValidationError
errors.As(err, &ve)
h.writeJSON(w, http.StatusUnprocessableEntity, ve.Fields)
default:
h.logger.Error("getting user", "error", err, "id", id)
http.Error(w, "internal error", http.StatusInternalServerError)
}
return
}
h.writeJSON(w, http.StatusOK, user)
}
Claude applies errors.Is for sentinel errors and errors.As for type-checked errors — the correct Go idiom.
Interfaces: Where and How
Go interfaces are small and defined where they’re needed. Claude Code follows this:
The UserService needs to send emails. I want to mock the email sender in tests.
Define the interface correctly — don't put it in the email package.
// In the users package (where it's used — not in the email package)
type EmailSender interface {
SendWelcome(ctx context.Context, to, name string) error
SendPasswordReset(ctx context.Context, to, token string) error
}
type UserService struct {
repo UserRepository // also an interface
email EmailSender
logger *slog.Logger
}
The interface is defined in the users package that consumes it. The email package implements it without knowing the interface exists. This is the Go convention — interfaces are satisfied implicitly, defined at point of use.
Concurrency: Goroutines and Channels
Go concurrency is powerful and subtle. Claude Code writes correct patterns:
Fan-out/Fan-in
I need to process 10,000 items concurrently, 20 at a time.
Collect all results and any errors.
func processAll(ctx context.Context, items []Item) ([]Result, error) {
const concurrency = 20
sem := make(chan struct{}, concurrency) // semaphore
results := make([]Result, len(items))
errs := make([]error, len(items))
var wg sync.WaitGroup
for i, item := range items {
wg.Add(1)
go func(i int, item Item) {
defer wg.Done()
sem <- struct{}{} // acquire
defer func() { <-sem }() // release
result, err := processItem(ctx, item)
results[i] = result
errs[i] = err
}(i, item)
}
wg.Wait()
// Collect errors
var errList []error
for _, err := range errs {
if err != nil {
errList = append(errList, err)
}
}
if len(errList) > 0 {
return nil, errors.Join(errList...)
}
return results, nil
}
Claude uses a semaphore (buffered channel) rather than a goroutine-per-item-with-limit, avoids the closure-over-loop-variable bug (explicitly passing i and item), and uses errors.Join (Go 1.20+) for multi-error collection.
Context Cancellation
Write a worker pool that respects context cancellation.
If the context is cancelled, stop processing new items
but let in-flight work finish.
Claude generates the correct select pattern with ctx.Done() — checking context before processing each item, not abandoning in-flight goroutines abruptly.
sync.Map vs sync.Mutex
I need a concurrent map for a cache. When should I use sync.Map vs
a map with a sync.RWMutex?
Claude gives the correct rule: sync.Map for write-once / read-many or when keys are disjoint (few writes per key); map + sync.RWMutex for general purpose (reads and writes to same keys). It writes the pattern that matches your actual use case.
Testing in Go
Go testing is table-driven by convention. Claude Code follows this:
Write tests for the getUserByID function.
Cover: found, not found, database error.
Use testify for assertions.
func TestGetUserByID(t *testing.T) {
tests := []struct {
name string
id int64
mockUser *User
mockErr error
wantErr bool
wantUser *User
}{
{
name: "found",
id: 1,
mockUser: &User{ID: 1, Name: "Alice"},
wantUser: &User{ID: 1, Name: "Alice"},
},
{
name: "not found",
id: 999,
mockErr: pgx.ErrNoRows,
wantErr: true,
},
{
name: "database error",
id: 1,
mockErr: errors.New("connection refused"),
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
repo := &mockUserRepo{user: tt.mockUser, err: tt.mockErr}
svc := NewUserService(repo, &mockEmailSender{}, slog.Default())
user, err := svc.GetUser(context.Background(), tt.id)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tt.wantUser, user)
})
}
}
Table-driven tests with subtests (t.Run) make the test output readable and make it easy to add new cases. Claude generates the mock types as unexported structs implementing the interface — the minimal mock pattern.
HTTP Services
Middleware
Write authentication middleware for chi that:
1. Extracts the Bearer token from Authorization header
2. Validates it against the database
3. Adds the user to the request context
4. Returns 401 if missing/invalid, 403 if user is inactive
Claude generates the middleware function with proper context key (non-exported type to avoid collisions), the helper UserFromContext function callers use to extract the user, and the 401/403 distinction.
Graceful Shutdown
Add graceful shutdown to the HTTP server.
On SIGTERM/SIGINT: stop accepting new connections,
wait up to 30s for in-flight requests to finish.
func run(ctx context.Context) error {
srv := &http.Server{Addr: ":8080", Handler: router}
go func() {
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
srv.Shutdown(shutdownCtx)
}()
if err := srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
return fmt.Errorf("server error: %w", err)
}
return nil
}
func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
if err := run(ctx); err != nil {
log.Fatal(err)
}
}
Claude uses signal.NotifyContext (Go 1.16+) for clean signal handling, runs shutdown in a goroutine so the main goroutine stays on ListenAndServe, and uses the 30-second deadline to avoid indefinite wait.
Go Modules and Workspace
I have 3 related Go services and some shared packages.
Should I use one module, multiple modules, or Go workspaces?
Explain the tradeoffs.
Claude gives a practical answer based on your deployment pattern: single module if services deploy together, multiple modules with Go workspace (go.work) for local development if services deploy independently. It explains that workspaces are development-only and don’t change the production module graph.
Performance Profiling with Claude Code
This function is showing up as hot in pprof profiles.
Here's the source and the allocation profile output.
Identify what's allocating and suggest fixes.
Claude reads the pprof output alongside the code and identifies specific allocation sources — usually string concatenation in loops (use strings.Builder), unnecessary interface boxing, or slice growth patterns (preallocate with make([]T, 0, knownLen)).
Testing, Benchmarks, and Fuzz Tests
For performance-critical code:
Add a benchmark for the parseCSV function
and a fuzz test for the parseDate function.
Claude writes BenchmarkParseCSV(b *testing.B) with proper b.ResetTimer() usage and FuzzParseDate(f *testing.F) with the seed corpus — the correct patterns for Go’s native benchmark and fuzz testing.
Using Claude Skills for Go
The Claude Skills 360 bundle includes Go-specific skills for patterns you use repeatedly:
- HTTP service scaffolding (handler + service + repository layers)
- Concurrency patterns (worker pools, rate limiters, circuit breakers)
- Database patterns (transaction management, connection pooling)
- CLI tool scaffolding with
cobraandviper
With a good CLAUDE.md and the right skills, Go development with Claude Code is fast and produces consistently idiomatic code. Start with the free tier and try the testing workflow — Go’s table-driven test generation is one of the best Claude Code use cases for any language.