Go
Go rules and best practices for Claude Code.
8.1k
- Go
- Docker
- Postgres
- Redis
- Grpc
- Kubernetes
5 TEMPLATES
Idiomatic Go Rules
# CLAUDE.md — Idiomatic Go ## Project layout - `cmd/<app>/main.go` per binary. - `internal/` for code that other modules can't import. - Top-level packages for public, importable code. - Don't create a `pkg/` folder — it adds noise and Go doesn't require it. - One package per directory. The package name matches the directory. ## Errors - Errors are values. Return `error` as the last return value of fallible functions. - Wrap with `%w` to keep the chain: `fmt.Errorf("save user %s: %w", id, err)`. - Check with `errors.Is` for sentinels and `errors.As` for typed matches. Don't compare error strings. - Define sentinel errors in the package that produces them: `var ErrNotFound = errors.New("not found")`. - Don't `panic` for control flow. `panic` is for unrecoverable bugs. ## Interfaces - Interfaces belong with the **consumer**, not the producer. The package that needs a `Reader` defines it. - Keep interfaces small. The narrower, the more useful. `io.Reader` is one method. - Don't write a "marker" interface — that's an OO instinct. ## Concurrency - Goroutines are cheap; lifetime management is not. Every `go func()` answers: who waits for it, who cancels it? - Use `context.Context` for cancellation. Pass it as the first parameter of any function that does I/O. - Use channels for ownership transfer; mutexes for shared state. Don't pick channels just because they're "more Go". - `sync.WaitGroup` for fan-out/fan-in. `errgroup.Group` when you also want to propagate the first error. ## Structs & methods - Method receivers: pointer for mutation or large structs; value for small immutable types. Be consistent within a type. - Constructors are `New<T>` returning a value or pointer. Return errors when construction can fail. - Tag struct fields for serialization once, at the boundary type — don't tag domain types. ## Standard library first - The stdlib does more than people remember. Reach for `net/http`, `database/sql`, `encoding/json`, `flag`, `log/slog` before pulling deps. - Pull deps for boilerplate-heavy areas: routers (`chi`), DB helpers (`sqlx`, `pgx`), assertions (`testify`). ## Testing - `go test ./...`. Table-driven tests with subtests: ```go for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { ... }) } ``` - Test files live next to the code (`foo.go` + `foo_test.go`). - Use `t.Helper()` in test helpers so failures point to the call site. - `testing/synctest` (Go 1.24+) or coordinated mocks for time- or concurrency-dependent tests. ## Style - `gofmt`, `goimports`, `staticcheck` in CI. No bikeshedding on style. - Names are short and clear. `i`, `n`, `r` in tight scope are fine; `userRepository` reads better than `userRepo` when you have several `repo`s. - Comments above exported identifiers start with the identifier name and end with a period. ## Don't - Don't define interfaces that aren't used yet ("future-proof"). Wait until you have a second implementation. - Don't use `init()` functions to do work. Make the dependency explicit. - Don't ignore `error` return values. `_ = ...` is a deliberate choice, not a default. - Don't return `nil, nil` from a function that returns a pointer and an error. Pick "found", "not found", or "errored".Go HTTP Server (net/http + chi)
# CLAUDE.md — Go HTTP Server (net/http + chi) ## Stack - `net/http` for the server, **chi** for routing (or `http.ServeMux` from Go 1.22+ if your routing is simple). - One handler function per route. Handlers are `func(w http.ResponseWriter, r *http.Request)`. - Don't build a custom HTTP framework. Compose middleware functions. ## Server bootstrap ```go srv := &http.Server{ Addr: ":" + port, Handler: routes(), ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second, IdleTimeout: 120 * time.Second, BaseContext: func(net.Listener) context.Context { return appCtx }, } ``` - Always set timeouts. The defaults are unbounded. - `BaseContext` carries app-level cancellation into every request. ## Graceful shutdown ```go go func() { if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { log.Fatal(err) } }() <-ctx.Done() shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() srv.Shutdown(shutdownCtx) ``` - Listen for `SIGTERM` / `SIGINT`, cancel `ctx`, call `Shutdown` with a timeout. - 30s grace is reasonable for human-facing services. Long-running streams need more or you cut them off. ## Routing with chi - Subrouters per resource: `r.Route("/users", func(r chi.Router) { ... })`. - Middleware is a `func(http.Handler) http.Handler` chain. Order: recovery → logging → request id → auth → routes. - Use chi's request id, logger, and recoverer middleware before writing your own. ## Request handling - Parse JSON with `json.NewDecoder(r.Body).Decode(&req)` and `defer r.Body.Close()` (for large bodies). - Validate after decoding. Return 400 with a JSON error on failure. - Set a max body size: `r.Body = http.MaxBytesReader(w, r.Body, 1<<20)` (1 MB) at the edge. ## Responses - One helper per response shape: ```go func writeJSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(v) } ``` - Set `Content-Type` before `WriteHeader`. After `WriteHeader`, headers are frozen. - Don't write the response twice. After an error path, `return`. ## Errors - Map domain errors to HTTP status in one place (the writer or a helper): ```go switch { case errors.Is(err, ErrNotFound): http.Error(w, "not found", 404) case errors.As(err, &v): http.Error(w, v.Message, 400) default: http.Error(w, "internal", 500) } ``` - Log the underlying error server-side. Don't echo it to clients. ## Auth - Middleware that extracts a user from the request, attaches to context with a typed key, and rejects unauthenticated. - Avoid `context.Value` sprawl. Encapsulate access: `userFromContext(r.Context()) (User, bool)`. ## Don't - Don't write to `http.ResponseWriter` after the handler returns — it's gone. - Don't ignore `r.Context()`. Pass it to every downstream call (DB, HTTP) so cancellation propagates. - Don't use `init()` to register routes. Build them explicitly. - Don't share `http.Client` instances with default settings — set timeouts.Go + sqlx + Postgres
# CLAUDE.md — Go + sqlx + Postgres ## Stack - **pgx** as the driver (use `stdlib` adapter for `database/sql`, or pgx native). - **sqlx** for ergonomic struct scanning and named parameters. - **golang-migrate** or **goose** for migrations. Pick one; commit to it. - No ORM — sqlx hits the sweet spot of typed but explicit. ## Connection ```go db, err := sqlx.Open("pgx", dsn) db.SetMaxOpenConns(25) db.SetMaxIdleConns(25) db.SetConnMaxLifetime(5 * time.Minute) db.SetConnMaxIdleTime(5 * time.Minute) ``` - Always set pool sizes. Defaults are unsuitable for any real load. - One `*sqlx.DB` per app, passed via constructor injection. ## Queries - Named parameters via `:name`: ```go rows, err := db.NamedQueryContext(ctx, `SELECT * FROM users WHERE id = :id`, map[string]any{"id": id}) ``` - Struct scanning with `db` tags: ```go type User struct { ID string `db:"id"` Email string `db:"email"` } err := db.GetContext(ctx, &user, `SELECT id, email FROM users WHERE id = $1`, id) ``` - Always pass `ctx`. A query without context can outlive its caller. ## Inserts & updates - Use `INSERT ... RETURNING` to fetch generated IDs in one round trip. - Bulk inserts: build a single multi-row `INSERT` or use `pgx.CopyFrom` for >1000 rows. - For upserts: `INSERT ... ON CONFLICT DO UPDATE`. Don't roundtrip "select then insert". ## Transactions ```go tx, err := db.BeginTxx(ctx, nil) if err != nil { return err } defer tx.Rollback() // no-op if committed // ... do work ... return tx.Commit() ``` - The `defer tx.Rollback()` pattern is safe: rollback after commit is a no-op. - Pass `*sqlx.Tx` (or an interface that both `*sqlx.DB` and `*sqlx.Tx` satisfy) to functions that should work in either. ## Migrations - Migrations are SQL files: `0001_create_users.up.sql`, `0001_create_users.down.sql`. - Apply on deploy as a separate step, not at app boot. - Idempotent SQL where possible (`CREATE TABLE IF NOT EXISTS`) — the migration tool tracks applied versions, but defensive SQL helps recover from bad states. ## Performance - Add indexes for every WHERE/ORDER you actually query. Validate with `EXPLAIN (ANALYZE, BUFFERS)`. - Connection acquisition is fast but not free — long-running transactions hold connections, which holds the pool. - For read-heavy workloads, consider a separate read pool pointed at a replica. ## Don't - Don't `db.Query` and forget to `Close` the rows. Use `defer rows.Close()` immediately after. - Don't build SQL with `fmt.Sprintf` and user input — that's SQL injection. Always parameters. - Don't ignore `rows.Err()` after the loop. The iteration error surfaces there. - Don't share a `*sqlx.Tx` across goroutines. It's not safe.Go Testing Rules
# CLAUDE.md — Go Testing Rules ## Layout - Test files live next to the code: `foo.go` + `foo_test.go`. - One package per directory. Use `package foo_test` (external test) when you only want to test the public API. - Helpers live in `*_test.go` files; they're not exported to consumers. ## Table-driven tests ```go func TestParse(t *testing.T) { cases := []struct { name string input string want Result wantErr bool }{ {"empty", "", Result{}, true}, {"simple", "a=1", Result{A: 1}, false}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { got, err := Parse(tc.input) if (err != nil) != tc.wantErr { t.Fatalf("err = %v, wantErr %v", err, tc.wantErr) } if got != tc.want { t.Errorf("got %v, want %v", got, tc.want) } }) } } ``` - Subtests via `t.Run` give clean names and let you target one (`go test -run TestParse/simple`). - `t.Fatalf` for setup failures, `t.Errorf` for assertions where you can keep going. ## Assertions - Standard library by default. Reach for **testify** (`require`, `assert`) when boilerplate dominates. - `require.NoError(t, err)` instead of `if err != nil { t.Fatal(err) }` is fine — it's the only `if` you write 100 times. - Avoid `reflect.DeepEqual` for floats — use a tolerance. ## Test helpers - Mark helpers with `t.Helper()` so failure lines point to the caller, not the helper. - Helpers take `*testing.T` first. Cleanup with `t.Cleanup(...)` — runs even if the test fails. ## Integration tests - Build tag for integration tests: `//go:build integration` at the top of the file. Run with `go test -tags=integration ./...`. - Spin up real Postgres / Redis with **testcontainers-go** or a local docker-compose. - Reset state between tests — truncate tables in `t.Cleanup`. ## Concurrency tests - `-race` in CI. Always. Don't merge without it. - For ordering, use `testing/synctest` (Go 1.24+) or carefully designed channels — not `time.Sleep`. ## Coverage - `go test -cover`. Aim for high coverage on services and parsing, not for 100%. - `-coverprofile=cover.out` + `go tool cover -html=cover.out` to find untested branches. ## Benchmarks - `func BenchmarkX(b *testing.B)` next to the function. - Use `b.ReportAllocs()` to surface allocations. - Compare with `benchstat` after a change. A single run is meaningless. ## Don't - Don't use `time.Sleep` to "wait for" something async. Use channels, `sync.WaitGroup`, or polling with a deadline. - Don't share mutable state across `t.Parallel()` tests. - Don't disable tests with `t.Skip` and leave them. Either fix them or delete them. - Don't rely on the order of map iteration. It's randomized.Go gRPC Services
# CLAUDE.md — Go gRPC Services ## Service definition - One `.proto` per service. Live in `proto/` at the repo root or in a separate buf module. - Use **buf** for codegen, lint, and breaking-change detection. Don't shell out to `protoc` directly. - Versioned packages: `package myapp.users.v1`. New incompatible API → new version, not a rewrite. ## Code generation - `buf generate` writes Go stubs into `gen/proto/...`. Commit the generated code so consumers don't need buf. - Lock plugin versions in `buf.gen.yaml`. Stub drift is a tax on every PR. ## Server skeleton ```go srv := grpc.NewServer( grpc.ChainUnaryInterceptor(loggingInterceptor, recoveryInterceptor, authInterceptor), grpc.MaxRecvMsgSize(4 * 1024 * 1024), ) v1.RegisterUsersServiceServer(srv, &usersServer{...}) reflection.Register(srv) ``` - Always register reflection in non-prod (debugging tools need it). Gate behind a flag in prod. - Set message size limits — the default 4 MB recv is sometimes too small, often too generous. ## Errors - Return `status.Error(codes.NotFound, "user %s", id)` — gRPC status codes map to HTTP and other transports. - Define a single helper that maps domain errors to gRPC codes. Don't sprinkle `status.Errorf` everywhere. - Detail proto messages for structured errors: use `errdetails.BadRequest`, `errdetails.PreconditionFailure`. ## Streaming - Server-stream for "long list" responses where pagination would round-trip too much. - Client-stream for batch ingestion (logs, metrics). - Bidi for chat-like or tightly coordinated flows. Don't reach for it without a reason. - Always honor `ctx.Done()` in stream loops — clients hang up. ## Interceptors - One unary chain, one stream chain. Order: recovery → logging → tracing → auth → app middleware. - Don't write business logic in interceptors. They're for cross-cutting concerns. ## Metadata & auth - Auth tokens go in metadata: `authorization: Bearer ...`. Read with `metadata.FromIncomingContext`. - Don't use the `Context` to carry app values across services. Carry them in the proto message. ## Performance - Prefer many small RPCs over one huge one. Latency is mostly serialization, not the network. - Use connection pooling (`grpc.NewClient` returns a multiplexed conn — share it). - Compression (`gzip`) only when payloads are large and the network is slow. Otherwise it's CPU for nothing. ## Testing - `bufconn` for in-memory gRPC tests: ```go lis := bufconn.Listen(1024 * 1024) go srv.Serve(lis) conn, _ := grpc.Dial("bufnet", grpc.WithContextDialer(...), grpc.WithTransportCredentials(insecure.NewCredentials())) ``` - Don't spin up a real listener for unit tests. ## Don't - Don't expose internal IDs as `string` when you have a typed enum or wrapper. Make protos express intent. - Don't break wire compatibility. Field tags are forever. - Don't mix gRPC concerns with domain types. Translate at the boundary. - Don't return raw `error` from server methods. Always a `status.Error`.
Have a CLAUDE.md template that works for you?
Send it in — we’ll credit you and publish it under the right tags.