Go is opinionated. There is one way to format code, a small set of idiomatic patterns for concurrency, and a strict error-handling convention that the language enforces through type system pressure rather than exceptions. That rigidity is one of Go’s greatest strengths — and the exact reason that AI coding agents need explicit instruction before they touch a Go codebase.
Without a CLAUDE.md, Claude Code working on a Go project will:
- Discard errors with
_in functions where the caller can’t know something failed - Forget
context.Contextin I/O functions, making cancellation impossible - Use
fmt.Printlnfor logging instead of the structuredlog/slogpackage - Start goroutines with no documentation of their lifecycle or cancellation contract
- Generate tests as one-off assertions rather than table-driven test cases
None of these are language errors. They all compile. They’re idiom violations — the kind that accumulate into a codebase that’s technically correct and practically unmaintainable.
A well-constructed CLAUDE.md closes that gap before it opens.
Why Go + Claude Code Needs a Thoughtful CLAUDE.md
Go’s conventions are learned through code review, not official specification. The language spec tells you what compiles; experienced Go engineers learn what survives production. That tacit knowledge is exactly what an AI agent lacks.
Three areas are where we see agents diverge most from idiomatic Go:
Error handling. Go’s error handling is explicit by design. Every function that can fail returns an error value. The idiomatic pattern is to check and return that error immediately, adding context with fmt.Errorf("operation: %w", err). Agents frequently skip the wrapping — returning bare err values that lose context — or, worse, log-and-continue when the caller expected a returned error.
Context propagation. context.Context is Go’s mechanism for cancellation, deadlines, and request-scoped values. It belongs as the first parameter in every function that does I/O, calls another service, or runs longer than a few milliseconds. Agents generated without context.Context awareness produce code that cannot be cancelled — which means goroutine leaks and cascading timeout failures in production.
Goroutine documentation. Go makes concurrency syntactically trivial (go fn()), which makes it easy to start goroutines with no clear ownership, lifecycle, or cancellation path. A CLAUDE.md that requires documentation of goroutine contracts surfaces this problem before code review.
Complete CLAUDE.md Template for Go Projects
This template is production-tested across CLI tools, HTTP services, and libraries. Copy, adjust the project-specific sections, and drop it in your repo root.
# Go Project: [ProjectName]
## Build & Run
- Build: `go build ./...`
- Test: `go test ./... -race`
- Lint: `golangci-lint run`
- Format: `gofmt -s -w .` or `goimports -w .`
- Vet: `go vet ./...`
- Mod tidy: `go mod tidy`
## Go Version
- Check go.mod for the current version; minimum is 1.22 for this project
- Use features available at that version; do not import packages from later stdlib
## Error Handling
- Always return errors; never discard with `_` in production code paths
- Wrap errors with context: `fmt.Errorf("operation name: %w", err)`
- Sentinel errors use exported vars: `var ErrNotFound = errors.New("not found")`
- Do not log an error and also return it; choose one
## Logging
- Use `log/slog` (available since Go 1.21), not `fmt.Println` or `log.Printf`
- Structured logs: `slog.Info("message", "key", value)`
- Debug logs: `slog.Debug(...)` — these are stripped in production by log level
- Do not use the global logger in library code; accept a `*slog.Logger` parameter
## Context Propagation
- Accept `context.Context` as the first parameter in all functions that do I/O
- Never store a context in a struct; pass it through function calls
- Always call `defer cancel()` immediately after `context.WithCancel` or `context.WithTimeout`
- Do not use `context.Background()` inside library functions; thread the context from the caller
## Naming Conventions
- Exported names: CamelCase (`GetUser`, `CreateOrder`, `ErrNotFound`)
- Single-method interfaces: end in `-er` (`Reader`, `Writer`, `Storer`, `Fetcher`)
- Unexported test helpers: lowercase, same package
- Package names: lowercase, single word, no underscores
## Testing
- Run all tests with race detector: `go test -race ./...`
- Prefer table-driven tests for any function with multiple input scenarios
- Use `testify/assert` and `testify/require` for assertions
- Integration tests: guard with `//go:build integration` build tag
- Mock via interfaces: define the interface in the package that uses it; mock it in tests
## Goroutine Safety
- Document which types are safe for concurrent use in the type's godoc
- Use `sync.Mutex` or `sync.RWMutex` for shared mutable state; document the lock field
- Prefer channels for coordination between goroutines
- Every goroutine must have a defined termination condition
- Never start a goroutine in a function without documenting its lifecycle
## Dependency Management
- Run `go mod tidy` after any dependency addition or removal
- Prefer standard library over third-party where the stdlib solution is adequate
- Vendor: use `go mod vendor` for containerized or air-gapped builds
- Check `go.sum` into version control
## HTTP Services (if applicable)
- Standard library `net/http` is preferred unless the project already uses Gin/Echo/Chi
- Middleware signature: `func(http.Handler) http.Handler`
- Responses: structured JSON, explicit HTTP status codes, never bare `200 OK` on errors
- Route registration: document the HTTP method and path in a comment above the handler
## Interfaces
- Define interfaces in the package that depends on the behavior, not the package that implements it
- Keep interfaces small; single-method interfaces are idiomatic
- Accept interfaces, return concrete types (in most cases)
Build and Test Commands
The commands section of your CLAUDE.md is what Claude Code uses to verify its own output. Get these right and Claude will self-correct after writes.
go test -race ./... is non-negotiable. Go’s race detector catches data races at runtime that static analysis cannot — including races that appear only under load. Adding -race to every test invocation in CLAUDE.md ensures Claude Code validates against it, not just go test.
For formatting, goimports -w . is strictly better than gofmt -s -w . in most projects. It formats code identically to gofmt and also manages import groups — separating stdlib, external, and internal imports. If you use goimports, say so explicitly in the template so Claude doesn’t emit bare gofmt commands.
Error Handling Rules
The error-handling section is where most Go-specific AI problems live.
The pattern to enforce is simple:
// Good: wrap with context, return
result, err := doSomething(ctx)
if err != nil {
return fmt.Errorf("doSomething: %w", err)
}
// Bad: discard
result, _ := doSomething(ctx)
// Bad: log and return — caller doesn't know it failed
if err != nil {
log.Printf("error: %v", err)
return result, nil // this is a lie
}
The third pattern — log-and-continue — is the most insidious because it compiles, passes tests, and silently swallows failures in production. Your CLAUDE.md should say explicitly: “Do not log an error and also return it; choose one.”
Sentinel errors (var ErrNotFound = errors.New("not found")) deserve their own line in the template. They’re the idiomatic Go equivalent of typed exceptions. Without explicit guidance, agents create ad-hoc string-compared errors or use nil returns where an error is semantically correct.
Context Propagation Rules
Context is Go’s most important AI-coding pitfall area. We analyzed dozens of AI-generated Go codebases and found that context handling is wrong more often than error handling.
The two rules that fix most problems:
First parameter, always. Every function that does I/O, makes an RPC call, queries a database, or runs a loop that could block must accept context.Context as its first parameter. Not buried in a struct. Not optional. First.
// Good
func FetchUser(ctx context.Context, id string) (*User, error)
// Bad — cannot be cancelled
func FetchUser(id string) (*User, error)
Never store in a struct. Contexts carry request-scoped data and deadlines. Storing them in a struct is a category error — it attaches a request’s lifetime to an object that may outlive it. The Go team says this explicitly in the stdlib docs. Put it in your CLAUDE.md so Claude Code doesn’t have to infer it.
// Bad
type UserService struct {
ctx context.Context // never do this
db *sql.DB
}
// Good — pass ctx per method call
func (s *UserService) Get(ctx context.Context, id string) (*User, error)
The defer cancel() rule is the third piece. Every context.WithCancel or context.WithTimeout creates a resource that must be released. The cancel function must be called or you leak. Pattern:
ctx, cancel := context.WithTimeout(parent, 5*time.Second)
defer cancel() // immediately after creation, always
Goroutine Safety Documentation
Goroutine safety is the one area where “just be careful” doesn’t scale. The CLAUDE.md approach is to require documentation as a code artifact.
Every type that is safe for concurrent use should say so in its godoc comment:
// UserCache is safe for concurrent use by multiple goroutines.
type UserCache struct {
mu sync.RWMutex
items map[string]*User
}
Every type that is not safe for concurrent use should say that too:
// Scanner is not safe for concurrent use.
type Scanner struct {
buf []byte
pos int
}
For goroutines specifically, the lifecycle contract matters:
// processor runs until ctx is cancelled.
// Callers should call cancel() when the processor is no longer needed.
// Use sync.WaitGroup to wait for completion before exiting.
func (p *Processor) Start(ctx context.Context, wg *sync.WaitGroup) {
wg.Add(1)
go func() {
defer wg.Done()
p.run(ctx)
}()
}
When your CLAUDE.md requires this documentation, Claude Code produces it. When it doesn’t, you get undocumented goroutines that are “probably fine.”
Interface-Driven Design
Go interfaces work differently from interfaces in Java or C#. They’re implicit — a type satisfies an interface just by having the right method set. This makes them powerful for testing and composition, but it means the relationship between interface and implementation is invisible without explicit structure.
The pattern that works best with Claude Code:
- Define the interface in the package that uses the behavior
- Let Claude implement to that interface
- Mock the interface in tests
// In package storage (the consumer)
type UserStore interface {
Get(ctx context.Context, id string) (*User, error)
Save(ctx context.Context, u *User) error
}
// In package postgres (the implementation)
type DB struct { /* ... */ }
func (d *DB) Get(ctx context.Context, id string) (*User, error) { /* ... */ }
func (d *DB) Save(ctx context.Context, u *User) error { /* ... */ }
Your CLAUDE.md should say: “Define interfaces in the package that depends on the behavior, not the package that implements it.” This is the Go standard (see io.Reader, http.Handler, sql.Scanner) and it’s what makes the dependency graph flow in the right direction.
golangci-lint Integration with Hooks
golangci-lint runs multiple static analysis tools in parallel. It catches things go vet misses — including some of the error-handling and context problems described above — if configured correctly.
Claude Code hooks run after file edits. Adding golangci-lint to your hook configuration creates a tight feedback loop:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "go vet ./... && golangci-lint run"
}
]
}
]
}
}
Place this in your project’s .claude/settings.json. After every file write, Claude Code will run go vet and golangci-lint — and if either fails, it will see the output and attempt to fix the issue before returning control to you.
A minimal .golangci.yml to pair with this:
linters:
enable:
- errcheck # catches discarded errors
- govet # catches common mistakes
- staticcheck # broad static analysis
- contextcheck # catches missing context propagation
- noctx # catches http requests missing context
- wrapcheck # enforces error wrapping
linters-settings:
errcheck:
check-type-assertions: true
check-blank: true
contextcheck and noctx specifically target the context propagation problems described earlier. wrapcheck enforces error wrapping. Combined with the CLAUDE.md rules, these linters catch what the instructions miss and the instructions catch what the linters miss.
go.mod and Dependency Policy
The dependency section of your CLAUDE.md prevents two common AI behaviors: adding unnecessary dependencies and leaving go.mod in a dirty state after changes.
Key rules to encode:
- Run `go mod tidy` after any dependency addition or removal
- Prefer standard library over third-party where the stdlib solution is adequate
- Do not add dependencies for: JSON marshaling (use encoding/json), HTTP (use net/http),
assertions in non-test code (return errors), UUID generation (use google/uuid or crypto/rand)
The “prefer stdlib” line is load-bearing. Agents default to importing familiar third-party packages (gjson, resty, etc.) even when the stdlib solution is adequate. Stating the preference explicitly changes the default behavior.
AGENTS.md Compatible Version
If you’re using an agent that reads AGENTS.md instead of or in addition to CLAUDE.md, here is a condensed version suitable for that format:
# AGENTS.md — Go Project
## Commands
- test: `go test -race ./...`
- lint: `golangci-lint run`
- format: `goimports -w .`
- vet: `go vet ./...`
- deps: `go mod tidy`
## Critical Rules
1. Return all errors; never discard with `_` in production code
2. Wrap errors: `fmt.Errorf("context: %w", err)`
3. Accept `context.Context` as first param in all I/O functions
4. Never store context.Context in a struct
5. Call `defer cancel()` immediately after any context with cancellation
6. Document goroutine lifecycle in comments
7. Table-driven tests for multi-case functions
8. Run `go mod tidy` after dependency changes
## Interface Pattern
- Define interfaces where they are used (consumer package), not where they are implemented
- Single-method interfaces use -er suffix (Storer, Fetcher, Parser)
- Accept interfaces, return concrete types
The AGENTS.md version is intentionally shorter. It captures the non-negotiables without the explanatory prose. Use it alongside CLAUDE.md if you want to support multiple agent runtimes.
Common AI + Go Mistakes to Watch For
These are the anti-patterns we see most often in AI-generated Go code, even with a CLAUDE.md in place. Watch for them in review:
Ignoring errors from Close and similar cleanup functions. Agents frequently write defer f.Close() without capturing the error. For write-heavy operations (file writes, database commits), a failed Close() means data loss.
// Bad
defer f.Close()
// Good
defer func() {
if err := f.Close(); err != nil {
slog.Error("close failed", "err", err)
}
}()
Creating goroutines inside HTTP handlers without context. HTTP handler contexts are cancelled when the client disconnects. A goroutine started inside a handler using context.Background() will run forever after the request completes.
Using init() for side effects. Agents reach for init() to initialize global state. This makes testing hard and import order relevant. Prefer explicit initialization in main() or constructor functions.
Map writes without mutex. Go maps are not safe for concurrent writes. An agent that adds a write operation to a map-based cache without noticing the existing sync.RWMutex creates a race condition that the race detector will catch — but only at runtime under concurrent load.
String formatting in hot paths. fmt.Sprintf allocates. In functions called thousands of times per second, agents use it reflexively. Your CLAUDE.md can note hot-path functions explicitly to prevent this.
The patterns above don’t require a complex setup. A CLAUDE.md in your repo root, a .golangci.yml with four or five linters, and a PostToolUse hook are enough to shift Claude Code’s Go output from “compiles but needs review” to “idiomatic and production-ready.”
Browse working CLAUDE.md examples from real Go projects in our gallery.
FAQ
Does Claude Code read go.mod automatically?
Yes. Claude Code indexes the files in your project, including go.mod. But indexing is not the same as understanding — the CLAUDE.md language version note tells Claude to actively check go.mod before using stdlib features that require a specific Go version.
Should I put CLAUDE.md in a library package or just the root?
Root is sufficient for most projects. For monorepos with multiple Go modules, a CLAUDE.md per module directory is cleaner than one root file trying to distinguish between service/ and lib/.
golangci-lint is slow. Does running it on every Edit make Claude Code unusable?
golangci-lint run with caching is fast — typically under 5 seconds on a warm cache. The first run after a clean cache is slower. You can limit the hook to run only on *.go files to avoid triggering on config changes.
What about generics (Go 1.18+)?
Add a section to your CLAUDE.md if your project uses generics. The main thing to specify is the constraint pattern you prefer (any vs explicit interface constraints) and whether generic types should be in a generic/ subpackage or co-located with their primary use case.
Can I use this template with Cursor or GitHub Copilot?
The AGENTS.md version above is compatible with any agent that reads that file. The CLAUDE.md version is Claude Code–specific but the content is universally useful — Cursor reads it too if you rename it to a cursor-compatible format.