Go Language Patterns
PLGo patterns for AI coding agents are prompts and verification gates that make Claude Code, Codex, and Cursor produce production-ready Go services: graceful shutdown, errgroup-bounded concurrency, sized pgx pools, table-driven and Testcontainers tests, and gRPC status mapping. Agent output is accepted on evidence from the race detector, goleak, golangci-lint, govulncheck, and a load test, not on line-by-line reading.
Your Go service handles 200 requests per second in staging, then falls over the first time real traffic hits it. The per-request goroutines never exit, the pgx pool starves because a rows.Close() is missing, and a downstream call with no deadline piles up until the pod is OOM-killed. The agent’s code looked idiomatic and passed its own happy-path tests. Nothing it produced was checked for the failure modes that only show up under concurrency.
This recipe is for developers who build Go backends with an agent and want to stop reviewing every goroutine by eye. It front-loads the production details into the prompts, then makes the machine prove them.
What you’ll walk away with from these Go recipes
Section titled “What you’ll walk away with from these Go recipes”- One Go rule block, in
AGENTS.mdfor Claude Code and Codex and mirrored in a Cursor project rule, plus amake checkgate the agent runs before it says “done” - Five copy-paste prompts: a chi HTTP server with graceful shutdown, a bounded
errgroupbatch, a pgx repository with sized pools and safe transactions, table-driven plus Testcontainers tests, and a gRPC service with status mapping - A verification table that tells you what each gate catches, so you read the gate output instead of the diff
- Recovery prompts for the six ways agent-written Go breaks in production
Set up Go conventions and a gate the agent must pass
Section titled “Set up Go conventions and a gate the agent must pass”Two files do most of the work: a rules file the agent reads on every task, and a single command that fails when the code is not production-ready. Write them once per repository, before you ask for any code.
-
Pin a supported toolchain in
go.mod. On 2026-09-26 the newest release is Go 1.27.1 (perproxy.golang.org), so Go 1.26 and 1.27 are the supported lines. An explicitgodirective stops the agent from reaching for pre-1.22 idioms.module github.com/yourorg/ordersgo 1.26require (github.com/go-chi/chi/v5 v5.3.2github.com/jackc/pgx/v5 v5.11.0github.com/stretchr/testify v1.12.1go.uber.org/goleak v1.3.0go.uber.org/zap v1.28.0golang.org/x/sync v0.23.0)The versions are the latest on
proxy.golang.orgon 2026-09-26. Letgo get -u ./...move them later; the point is that the agent sees current majors. -
Add the gate as a
make checktarget. It is the definition of done for every prompt on this page..PHONY: checkcheck:test -z "$$(gofmt -l .)"go vet ./...golangci-lint run ./...go test -race -shuffle=on -count=1 ./...govulncheck ./...Install the two external tools once (terminal):
Terminal window go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.14.0go install golang.org/x/vuln/cmd/govulncheck@latest -
Put the Go rules in
AGENTS.mdat the repository root. Codex readsAGENTS.mdnatively; the tool tabs below show how Claude Code loads the same file and how to mirror it as a Cursor rule.## Go conventions- Go 1.26+. Loop variables are per-iteration: never write `x := x` copies.- Layout: cmd/, internal/. No package-level mutable state.- Every blocking function takes ctx context.Context first. Never callcontext.Background() outside main() and tests.- Every `go` statement has a documented exit path on ctx cancellation.Prefer errgroup with SetLimit over raw goroutines.- Wrap errors with fmt.Errorf("op: %w", err). Compare with errors.Is/As, never ==.- Transactions use pgx.BeginFunc; do not hand-roll commit/rollback.- Tests are table-driven with t.Parallel(); use testify require/assert.- Never edit an existing *_test.go assertion or add t.Skip to make a test pass.If a test looks wrong, stop and say why.- Done means `make check` passes. Paste its last 20 lines in your final message.
The last two rules matter most. The first keeps the agent from weakening your tests to reach green; see protect the oracle for why that is the most common way agent work fakes success. The second turns “I think it works” into evidence you can read in ten seconds.
Wire the rules and gate into each tool
Section titled “Wire the rules and gate into each tool”The rules file and the prompts are identical across tools. What differs is how each tool loads the rules, runs the gate, and reviews the result.
From v2.1.277 (both release channels on 2026-10-12) Claude Code reads AGENTS.md when no CLAUDE.md exists. If you already have a CLAUDE.md, add the import. The @path import syntax expands the file into context at launch:
@AGENTS.mdFormat every Go file the agent edits with a PostToolUse hook in .claude/settings.json:
{ "hooks": { "PostToolUse": [ { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": "jq -r '.tool_input.file_path' | grep '\\.go$' | xargs -r gofmt -w" } ] } ] }}Give the agent type-aware navigation with the gopls-lsp plugin from Anthropic’s official marketplace. The plugin registers the server only, so install gopls first:
go install golang.org/x/tools/gopls@latestclaude plugin install gopls-lsp@claude-plugins-officialBefore you open a pull request, run /code-review in the session for a second opinion on the diff.
Codex reads AGENTS.md directly, so no extra file is needed. Add the MCP server built into gopls (the gopls mcp subcommand, present in gopls v0.23.0) so the agent can query symbols, references, and diagnostics instead of grepping:
go install golang.org/x/tools/gopls@latestcodex mcp add gopls -- gopls mcpRun a task interactively with workspace writes and approval prompts for anything wider:
codex -s workspace-write -a on-request "Implement ProcessBatch as described in docs/tasks/batch.md. Done means make check passes."Review the branch before you push it:
codex review --base mainCreate a project rule in Cursor’s Rules settings with the same list as AGENTS.md, scoped to Go files, so the agent loads it whenever it touches a .go file. Add the gopls MCP server in Cursor’s MCP configuration (.cursor/mcp.json):
{ "mcpServers": { "gopls": { "command": "gopls", "args": ["mcp"] } }}Use Plan Mode for the multi-file scaffolds on this page so you approve the file list before any code is written. Run independent tasks in Worktrees, and let Bugbot review the pull request.
Start each tool on its default model and raise the effort setting before you switch models; the models hub lists the current defaults.
Build a production Go service with an agent, step by step
Section titled “Build a production Go service with an agent, step by step”Each step below is one prompt, one excerpt of what a good answer looks like, and the gate that proves it. Run them in order: later prompts assume the server and repository exist.
How do you scaffold a Go HTTP server that shuts down cleanly?
Section titled “How do you scaffold a Go HTTP server that shuts down cleanly?”The most common first-pass omission is shutdown. The agent starts ListenAndServe in a goroutine and exits without draining in-flight requests, so every deploy drops traffic.
A good answer has this core:
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)defer stop()
go func() { if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { logger.Fatal("listen", zap.Error(err)) }}()
<-ctx.Done()shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)defer cancel()if err := srv.Shutdown(shutdownCtx); err != nil { logger.Error("shutdown", zap.Error(err))}You do not need to audit this by eye. The shutdown test fails if Shutdown reuses the cancelled ctx, because Shutdown then returns context.Canceled before the in-flight request finishes. The server timeouts matter because http.Server has no read or write deadline by default, and go vet will not tell you that.
How do you bound goroutines so a batch cannot exhaust the database?
Section titled “How do you bound goroutines so a batch cannot exhaust the database?”“Process these items concurrently” produces an unbounded go func() fan-out. Under load it opens one database connection per item and exhausts the pool or the file-descriptor limit.
func (s *Service) ProcessBatch(ctx context.Context, items []Item) error { g, ctx := errgroup.WithContext(ctx) g.SetLimit(8) for _, item := range items { g.Go(func() error { if err := ctx.Err(); err != nil { return err } return s.processItem(ctx, item) }) } if err := g.Wait(); err != nil { return fmt.Errorf("process batch: %w", err) } return nil}The proof here is goleak.VerifyTestMain: it fails the package if any goroutine is still running when the tests finish. Combined with -race in make check, it catches the two concurrency bugs you cannot see in a diff.
How do you get an agent to write a pgx repository that survives load?
Section titled “How do you get an agent to write a pgx repository that survives load?”Pool exhaustion is the classic “works in dev, dies in prod” bug. One detail agents rarely know: pgxpool’s default MaxConns is the greater of 4 and runtime.NumCPU() (pgx v5.11.0 source), so a two-vCPU container gets four connections unless you size the pool.
The prompt names pgx.BeginFunc on purpose. Left alone, agents hand-roll a WithTx helper with a deferred recover() and a named return, which is correct only if every branch is right. BeginFunc commits when the callback returns nil and rolls back otherwise, including when the callback panics, so there is less agent code for anyone to review.
The prompt also asks for 23505, not pgx.ErrNoRows, for duplicate emails. An INSERT that hits a unique constraint returns a *pgconn.PgError; it never returns ErrNoRows, and mapping the wrong one produces a 500 where the client expected a 409.
To let the agent read your real schema instead of guessing column names, connect a database MCP server to a development database only. The database MCP guide compares the options and their read-only switches. Do not use @modelcontextprotocol/server-postgres: the package is deprecated on npm and archived.
How do you generate Go tests that catch real bugs?
Section titled “How do you generate Go tests that catch real bugs?”Name the cases, or you get happy-path-only coverage. Then add one integration test against a real Postgres, because a mocked repository cannot catch a wrong SQL column or a missing unique index.
The function names in the second prompt are from testcontainers-go v0.44.0 (checked 2026-09-26). The older RunContainer entry point still compiles, so without the explicit names the agent often reaches for it.
Asking the agent to list the cases before the code gives you the one thing worth reading: the test list. If the list covers the behaviour you care about and make check is green, you have not needed to read the test bodies. For more on test design with agents, see unit testing with agents and integration testing.
How do you stand up a gRPC service without leaking internal errors?
Section titled “How do you stand up a gRPC service without leaking internal errors?”A generated handler that returns the raw err can send a client the failing SQL statement, so ask for one function that owns the mapping from domain errors to gRPC status codes.
A good answer has this core:
func toStatus(err error) error { switch { case errors.Is(err, users.ErrNotFound): return status.Error(codes.NotFound, "user not found") case errors.Is(err, users.ErrDuplicateEmail): return status.Error(codes.AlreadyExists, "email already registered") case errors.Is(err, context.DeadlineExceeded): return status.Error(codes.DeadlineExceeded, "deadline exceeded") default: slog.Error("unmapped error", "err", err) // detail stays in your logs return status.Error(codes.Internal, "internal error") }}The mapping function is the part that matters: every error leaves the service through it, and the default branch keeps internal detail in your logs instead of in the client’s response. The bufconn tests prove every mapping without a network or a human reading the handlers.
How do you verify agent-written Go without reading every line?
Section titled “How do you verify agent-written Go without reading every line?”Read the gate output and the test list, not the diff. Each gate below catches a class of bug that looks fine in review, which is why reading is the weakest check for Go concurrency code.
| Gate | Command | What it catches |
|---|---|---|
| Format and vet | gofmt -l ., go vet ./... | Unformatted files, misused Printf verbs, copied locks, unreachable code |
| Lint | golangci-lint run ./... | Unchecked errors (errcheck), ineffective assignments, staticcheck findings; == on wrapped errors once you enable errorlint |
| Race detector | go test -race -shuffle=on -count=1 ./... | Data races and order-dependent tests |
| Goroutine leaks | goleak.VerifyTestMain(m) in each concurrent package | Goroutines with no exit path |
| Vulnerabilities | govulncheck ./... | Known CVEs in code paths you actually call |
| Integration | Testcontainers tests without -short | Wrong SQL, missing constraints, bad migrations |
| Load | Twice expected peak against staging, for example echo "GET https://staging.example.com/api/v1/items" | vegeta attack -rate=400 -duration=60s | vegeta report | Pool exhaustion, missing timeouts, memory growth |
| Review agent | /code-review (Claude Code), codex review --base main (Codex), Bugbot (Cursor) | Logic errors and missing cases the gates cannot express |
Run the first six in CI on every pull request; see CI/CD pipeline patterns for the workflow file. The engineer who owns the service signs off on three things: the test-case list, a green gate, and the load-test graph. Human reading goes to the hunks the review agent flags and to any code that touches transactions or goroutines. Agent PR review covers how to split that work across a team.
When agent-written Go breaks in production
Section titled “When agent-written Go breaks in production”Each failure below passes a happy-path test suite. Each one has a symptom you can detect and a recovery you can paste.
-
Goroutine leak. Symptom:
go_goroutinesin your metrics climbs and never falls. Cause: ago func()blocked on a channel send with no receiver, or with noctx.Done()case. Recovery: addgoleak.VerifyTestMainto the package, then prompt: “Audit everygostatement in this package. For each one, name its exit path under context cancellation, and fix any that have none. Done meansmake checkpasses.” -
Context dropped mid-stack. Symptom: requests keep running after the client disconnects, and deadlines do nothing. Cause:
context.Background()orcontext.TODO()deep in a handler. Recovery:grep -rn "context.Background()\|context.TODO()" --include=*.go internal/and prompt the agent to threadctxthrough every function it finds. -
Pool exhaustion. Symptom: latency spikes, then requests hang waiting to acquire a connection. Cause: a missing
rows.Close(), a query onpoolinside a transaction (two connections per request), or the default pool size. Recovery: setMaxConnsdeliberately, checkpool.Stat()in a debug endpoint, and prompt: “Find everyQuerycall whose rows are not closed on every path, and every pool query made while apgx.Txis open.” -
Stale idioms from old training data. Symptom:
item := itemcopies,ioutilimports, orinterface{}everywhere. Cause: the model is drawing on pre-1.22 code. Recovery: thegodirective and theAGENTS.mdrule prevent most of it; for the rest, prompt: “Modernise this package for Go 1.26: remove loop-variable copies, replaceioutil, and useany. Change no behaviour.” -
The agent weakens a test to reach green. Symptom: an assertion loosened, a case deleted, or a
t.Skipadded in the same diff as the fix. Recovery: revert the test file, restate the rule fromAGENTS.md, and make CI fail when a pull request modifies, renames or deletes an existing*_test.gowithout atest-changelabel. The step is only a gate if the pull request cannot remove it: GitHub runs the workflow file from the pull request itself, so put/.github/underCODEOWNERSand require that review in branch protection or a ruleset. This step assumesactions/checkoutwithfetch-depth: 0; Protect the oracle covers the wider policy.- name: Block edits to existing tests without a test-change labelif: ${{ !contains(github.event.pull_request.labels.*.name, 'test-change') }}# Needs actions/checkout with fetch-depth: 0: origin/main... diffs from the merge base.run: |# --name-status lists a rename as R<score> OLD NEW; field 2 is the path that existed beforeif git diff --name-status --diff-filter=MDR origin/main... | awk -F'\t' '{print $2}' | grep '_test\.go$'; thenecho "Existing tests changed without the test-change label"; exit 1fi -
golangci-lint fails before it lints. Symptom: an error that the Go version used to build golangci-lint is lower than the targeted version, or a config error on a
.golangci.ymlthe agent wrote in the v1 format. Recovery: install a golangci-lint v2 built with a Go release at least as new as yourgodirective, and rungolangci-lint migrateon an old config.
Where to go next with Go services
Section titled “Where to go next with Go services”- Make the repository agent-ready first, if it is not yet: agent-ready codebase.
- Harden the data layer: SQL optimization patterns and database migration patterns.
- Ship the service: Docker patterns and Kubernetes patterns.
- Decide what the agent may run unattended: permissions and sandboxing.
- Compare with another compiled backend: Rust development patterns, or return to the backend recipes overview.
Did this guide help you?
One tap, no sign-in.
What will you do with it?
How did it read?
What got in the way? Pick any that apply
Your role?
Thanks. I read every answer. Michał Jaskólski
Did this guide help?
One tap each. Close whenever you like.
no sign-in