Skip to content

Go 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.md for Claude Code and Codex and mirrored in a Cursor project rule, plus a make check gate the agent runs before it says “done”
  • Five copy-paste prompts: a chi HTTP server with graceful shutdown, a bounded errgroup batch, 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.

  1. Pin a supported toolchain in go.mod. On 2026-09-26 the newest release is Go 1.27.1 (per proxy.golang.org), so Go 1.26 and 1.27 are the supported lines. An explicit go directive stops the agent from reaching for pre-1.22 idioms.

    module github.com/yourorg/orders
    go 1.26
    require (
    github.com/go-chi/chi/v5 v5.3.2
    github.com/jackc/pgx/v5 v5.11.0
    github.com/stretchr/testify v1.12.1
    go.uber.org/goleak v1.3.0
    go.uber.org/zap v1.28.0
    golang.org/x/sync v0.23.0
    )

    The versions are the latest on proxy.golang.org on 2026-09-26. Let go get -u ./... move them later; the point is that the agent sees current majors.

  2. Add the gate as a make check target. It is the definition of done for every prompt on this page.

    .PHONY: check
    check:
    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.0
    go install golang.org/x/vuln/cmd/govulncheck@latest
  3. Put the Go rules in AGENTS.md at the repository root. Codex reads AGENTS.md natively; 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 call
    context.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.

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.md

Format 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:

Terminal window
go install golang.org/x/tools/gopls@latest
claude plugin install gopls-lsp@claude-plugins-official

Before you open a pull request, run /code-review in the session for a second opinion on the diff.

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.

GateCommandWhat it catches
Format and vetgofmt -l ., go vet ./...Unformatted files, misused Printf verbs, copied locks, unreachable code
Lintgolangci-lint run ./...Unchecked errors (errcheck), ineffective assignments, staticcheck findings; == on wrapped errors once you enable errorlint
Race detectorgo test -race -shuffle=on -count=1 ./...Data races and order-dependent tests
Goroutine leaksgoleak.VerifyTestMain(m) in each concurrent packageGoroutines with no exit path
Vulnerabilitiesgovulncheck ./...Known CVEs in code paths you actually call
IntegrationTestcontainers tests without -shortWrong SQL, missing constraints, bad migrations
LoadTwice expected peak against staging, for example echo "GET https://staging.example.com/api/v1/items" | vegeta attack -rate=400 -duration=60s | vegeta reportPool 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_goroutines in your metrics climbs and never falls. Cause: a go func() blocked on a channel send with no receiver, or with no ctx.Done() case. Recovery: add goleak.VerifyTestMain to the package, then prompt: “Audit every go statement in this package. For each one, name its exit path under context cancellation, and fix any that have none. Done means make check passes.”

  • Context dropped mid-stack. Symptom: requests keep running after the client disconnects, and deadlines do nothing. Cause: context.Background() or context.TODO() deep in a handler. Recovery: grep -rn "context.Background()\|context.TODO()" --include=*.go internal/ and prompt the agent to thread ctx through every function it finds.

  • Pool exhaustion. Symptom: latency spikes, then requests hang waiting to acquire a connection. Cause: a missing rows.Close(), a query on pool inside a transaction (two connections per request), or the default pool size. Recovery: set MaxConns deliberately, check pool.Stat() in a debug endpoint, and prompt: “Find every Query call whose rows are not closed on every path, and every pool query made while a pgx.Tx is open.”

  • Stale idioms from old training data. Symptom: item := item copies, ioutil imports, or interface{} everywhere. Cause: the model is drawing on pre-1.22 code. Recovery: the go directive and the AGENTS.md rule prevent most of it; for the rest, prompt: “Modernise this package for Go 1.26: remove loop-variable copies, replace ioutil, and use any. Change no behaviour.”

  • The agent weakens a test to reach green. Symptom: an assertion loosened, a case deleted, or a t.Skip added in the same diff as the fix. Recovery: revert the test file, restate the rule from AGENTS.md, and make CI fail when a pull request modifies, renames or deletes an existing *_test.go without a test-change label. 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/ under CODEOWNERS and require that review in branch protection or a ruleset. This step assumes actions/checkout with fetch-depth: 0; Protect the oracle covers the wider policy.

    - name: Block edits to existing tests without a test-change label
    if: ${{ !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 before
    if git diff --name-status --diff-filter=MDR origin/main... | awk -F'\t' '{print $2}' | grep '_test\.go$'; then
    echo "Existing tests changed without the test-change label"; exit 1
    fi
  • 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.yml the agent wrote in the v1 format. Recovery: install a golangci-lint v2 built with a Go release at least as new as your go directive, and run golangci-lint migrate on an old config.

Did this guide help you?

One tap, no sign-in.

not at all a lot