Go Microservice
Claude Code instructions for Go microservices covering layering, context propagation, and reliability patterns.
Install path
Use this file for each supported tool in your project.
- Claude Code: Save as
CLAUDE.mdin your project atCLAUDE.md.
Configuration
CLAUDE.md
1# Go Microservice23HTTP service in Go. Prioritize small interfaces, explicit error handling, and production-safe defaults. Every change must pass `go vet`, `golangci-lint run`, and `go test ./...` before committing.45## Quick Reference67| Task | Command |8|---|---|9| Run service | `go run ./cmd/server` |10| Run all tests | `go test ./... -race -count=1` |11| Run single package tests | `go test ./internal/order/... -v` |12| Integration tests only | `go test ./... -tags=integration -race` |13| Lint | `golangci-lint run ./...` |14| Vet | `go vet ./...` |15| Build binary | `go build -o bin/server ./cmd/server` |16| Generate (mocks, sqlc, etc.) | `go generate ./...` |17| Tidy deps | `go mod tidy` |18| Docker up (full stack) | `docker compose up -d` |19| Docker rebuild | `docker compose up -d --build` |20| Migrate up (goose) | `goose -dir migrations postgres "$DATABASE_URL" up` |21| Migrate create (goose) | `goose -dir migrations create <name> sql` |22| Migrate up (atlas) | `atlas migrate apply --url "$DATABASE_URL"` |23| Migrate up (golang-migrate) | `migrate -path migrations -database "$DATABASE_URL" up` |2425Pick the migration tool that already exists in `go.mod`. If none, prefer goose.2627## Project Structure2829```30├── cmd/31│ └── server/ # main.go — wires everything, starts HTTP32├── internal/33│ ├── config/ # env parsing (envconfig / viper / koanf)34│ ├── handler/ # HTTP handlers — thin, call services35│ ├── middleware/ # auth, logging, recovery, request-id36│ ├── service/ # business logic — no HTTP concepts37│ ├── repository/ # data access — one file per aggregate38│ ├── model/ # domain types, value objects39│ └── platform/ # infra wiring: DB, cache, messaging clients40├── migrations/ # SQL migration files (sequential numbered)41├── pkg/ # Exported utilities (only if truly reusable)42├── api/ # OpenAPI specs, proto files43├── scripts/ # Dev and CI helper scripts44├── .golangci.yml # Linter config45├── docker-compose.yml46├── Dockerfile47└── Makefile48```4950`internal/` is the default. Only move to `pkg/` when another service imports it. Handler → Service → Repository is the dependency direction. Never skip layers.5152## Code Conventions5354### Errors5556- Wrap errors with `fmt.Errorf("operation context: %w", err)` — always use `%w` for wrapping.57- Define domain errors as sentinel values in `internal/model/errors.go`:58 ```go59 var (60 ErrNotFound = errors.New("not found")61 ErrConflict = errors.New("conflict")62 ErrUnauthorized = errors.New("unauthorized")63 )64 ```65- Handlers map domain errors to HTTP status codes. Services never import `net/http`.66- Check with `errors.Is()` and `errors.As()`, never compare strings.6768### context.Context6970- First parameter to every exported function: `ctx context.Context`.71- Pass context through the entire call chain: handler → service → repository → DB query.72- Attach request-scoped values (request ID, user ID, trace ID) via middleware.73- Never store `context.Context` in a struct.74- Use `context.WithTimeout` for outbound calls (HTTP, DB, gRPC) — 5s default for DB, 10s for external APIs.7576### Logging7778- Use structured logging: `log/slog` (stdlib, Go 1.21+). Fall back to `zerolog` or `zap` if already in `go.mod`.79- Always log with context: `slog.InfoContext(ctx, "order created", "order_id", id)`.80- Log at handler entry/exit and on errors. Do not log in hot loops.81- Levels: `Debug` for dev tracing, `Info` for business events, `Warn` for recoverable issues, `Error` for failures requiring attention.82- Never log credentials, tokens, PII, or full request bodies.8384### Configuration & Environment8586- All config via environment variables. Parse in `internal/config/` at startup.87- Use `envconfig`, `koanf`, or `viper` — whichever is in `go.mod`. If none, use `envconfig`.88- Fail fast on missing required config: validate in `config.Load()`, return error, crash in `main()`.89- No globals for config. Pass config struct (or relevant subset) via constructor injection.9091### Dependency Injection9293- Constructor injection, no framework. Wire in `cmd/server/main.go`:94 ```go95 repo := repository.NewOrderRepo(db)96 svc := service.NewOrderService(repo, logger)97 h := handler.NewOrderHandler(svc, logger)98 ```99- Accept interfaces, return structs.100- Keep interfaces small — 1-3 methods. Define interfaces where they're used (in the consumer package), not where they're implemented.101102### HTTP Handlers103104- Detect the router in `go.mod` (chi / gin / echo / stdlib `net/http`) and follow its patterns.105- Handlers decode request → call service → encode response. No business logic.106- Always set `Content-Type: application/json` for JSON responses.107- Return consistent error envelope:108 ```json109 {"error": {"code": "not_found", "message": "order 123 not found"}}110 ```111- Middleware order: recovery → request-id → logging → auth → rate-limit.112113### Database114115- Use `pgx` (preferred) or `database/sql` for Postgres. Use `sqlc` for type-safe SQL if present.116- One `*pgxpool.Pool` created in `main()`, passed down.117- Transactions live in the service layer:118 ```go119 func (s *OrderService) PlaceOrder(ctx context.Context, req PlaceOrderReq) error {120 tx, err := s.pool.Begin(ctx)121 if err != nil { return fmt.Errorf("begin tx: %w", err) }122 defer tx.Rollback(ctx)123 // ... repo calls using tx ...124 return tx.Commit(ctx)125 }126 ```127- Always use query parameters (`$1`, `$2`), never string concatenation.128- Close rows: `defer rows.Close()`.129130### Timeouts & Graceful Shutdown131132- `http.Server.ReadTimeout`: 5s. `WriteTimeout`: 10s. `IdleTimeout`: 120s.133- Graceful shutdown in `main()`:134 ```go135 ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)136 defer stop()137 // ... start server ...138 <-ctx.Done()139 shutdownCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)140 defer cancel()141 srv.Shutdown(shutdownCtx)142 ```143144## Testing Strategy145146### Unit Tests147148- Colocate with source: `order_service.go` → `order_service_test.go` in the same package.149- Table-driven tests with `t.Run` subtests:150 ```go151 tests := []struct {152 name string153 input PlaceOrderReq154 wantErr error155 }{156 {"valid order", PlaceOrderReq{...}, nil},157 {"empty items", PlaceOrderReq{}, ErrValidation},158 }159 for _, tt := range tests {160 t.Run(tt.name, func(t *testing.T) {161 err := svc.PlaceOrder(ctx, tt.input)162 assert.ErrorIs(t, err, tt.wantErr)163 })164 }165 ```166- Mock interfaces with hand-written mocks or `gomock`/`mockery` if already in use.167- Use `testify/assert` and `testify/require` for assertions — `require` for fatal, `assert` for non-fatal.168169### Integration Tests170171- Guard with build tag: `//go:build integration`.172- Use `testcontainers-go` for Postgres, Redis, etc.:173 ```go174 func TestOrderRepo_Integration(t *testing.T) {175 if testing.Short() { t.Skip("skipping integration test") }176 ctx := context.Background()177 pg, err := postgres.Run(ctx, "postgres:16-alpine")178 require.NoError(t, err)179 t.Cleanup(func() { pg.Terminate(ctx) })180 // ... run migrations, test repo methods ...181 }182 ```183- Run migrations against the test container before assertions.184- Each test gets a fresh DB or uses transactions that roll back.185186### Test Naming187188- Functions: `Test<Type>_<Method>_<scenario>` — e.g. `TestOrderService_PlaceOrder_EmptyItems`.189- Files: `<source>_test.go`.190191### What to Test192193- Services: all business rules, edge cases, error paths.194- Repositories: integration tests against real DB.195- Handlers: request decoding, status codes, error mapping. Use `httptest.NewRecorder()`.196- Skip testing: auto-generated code, simple struct definitions, `main()`.197198## When Using Claude Code199200### Exploring the Codebase201202Before making changes, orient yourself:203204```205# Find the router setup206rg "NewRouter\|NewMux\|gin.New\|echo.New\|chi.NewRouter" cmd/ internal/207208# Find where an entity is defined209rg "type Order struct" internal/210211# Check existing patterns for a new handler212cat internal/handler/order.go213214# See what interfaces a service depends on215rg "type.*interface" internal/service/216217# Check migration history218ls -la migrations/219```220221### Making Changes2222231. **Read first.** Before writing code, read the relevant handler, service, and repository files. Understand the existing pattern before replicating it.2242. **Follow existing patterns.** If the repo uses `chi`, don't introduce `gin`. If errors are wrapped with `fmt.Errorf`, don't switch to `pkg/errors`. Match what's there.2253. **Run checks after every change:**226 ```bash227 go vet ./...228 golangci-lint run ./...229 go test ./... -race -count=1230 ```2314. **One concern per commit.** Don't mix a refactor with a feature. Commit the migration separately from the handler.2325. **Generate after schema changes.** If you modify a `.sql` query file or proto, run `go generate ./...` and include generated files in the commit.233234### When to Ask vs. Proceed235236**Just do it:**237- Adding a new handler that follows an existing pattern238- Writing tests for existing code239- Fixing lint errors240- Adding error wrapping241- Creating a migration for a new table242243**Ask first:**244- Changing the router, logger, or DB driver245- Adding a new dependency to `go.mod`246- Modifying middleware that affects all routes247- Changing the project structure or package layout248- Anything that touches auth/authz logic249250### Git Workflow251252- Branch from `main`. Name: `feat/<thing>`, `fix/<thing>`, `chore/<thing>`.253- Write a clear commit message: imperative mood, reference ticket if exists.254- Before pushing, always run the full check suite:255 ```bash256 go mod tidy257 go vet ./...258 golangci-lint run ./...259 go test ./... -race -count=1260 ```261- If tests fail, fix them before committing. Do not skip or comment out tests.262263### Common Pitfalls264265- **Forgetting `-race` flag.** Always test with `-race`. Data races in Go are silent killers.266- **Nil pointer on zero-value structs.** Check for nil before dereferencing, especially from DB queries.267- **Goroutine leaks.** If you spawn a goroutine, ensure it has a shutdown path via context cancellation or channel close.268- **Import cycles.** If you hit one, you're probably putting interfaces in the wrong package. Move the interface to the consumer.269- **Exported types in `internal/`.** This is fine — `internal/` restricts external imports, not internal cross-package use.270
Community feedback
0 found this helpful
Works with: