Go rules for AI coding agents

Go is simple to read, which makes agent mistakes easy to miss in review: an error assigned to _, a goroutine with no way to stop, http.Get in production code with no timeout, a query built with fmt.Sprintf. The Go rules cover modules and services and are organised around errors, interfaces, concurrency, HTTP and I/O, project structure and tests.

They begin with the go directive in go.mod, because what is idiomatic depends on it: since Go 1.22 loop variables are per iteration, so the old v := v copy is noise, and since 1.21 min, max, clear and the slices and maps packages exist. For concurrency the rule is blunt: before writing go func(), decide what stops it.

Folder: rules/go/ · go.mdc, CLAUDE.md, AGENTS.md

What the Go rules tell your agent

First rule of each section in go.mdc, quoted as shipped.

Mistakes these rules are written to prevent

Source: “Concurrency”.

Checks the agent runs before it says done

Example commands from the Go file (swap in your repo's own):

Run gofmt -l . (expect no output), go vet ./..., go test -race ./... and the linter if configured. Report each command's result.

Before and after: Go: error wrapping and inspection review

Prompt card 17 · Stack: Go · Type: Review and refactor · Best model: a fast small model handles single files; a frontier reasoning model for packages

What the card is for

Make errors carry context without losing their identity: wrap with %w, inspect with errors.Is and errors.As, define sentinels or typed errors for conditions callers branch on, and stop string matching.

Worked example from the card

Before

func (s *Store) GetUser(ctx context.Context, id int64) (*User, error) {
	row := s.db.QueryRowContext(ctx, `SELECT id, email FROM users WHERE id = $1`, id)
	var u User
	if err := row.Scan(&u.ID, &u.Email); err != nil {
		log.Printf("scan failed: %v", err)
		return nil, fmt.Errorf("Failed to get user: %v", err)
	}
	return &u, nil
}

// handler
u, err := store.GetUser(ctx, id)
if err != nil && strings.Contains(err.Error(), "no rows") {
	http.Error(w, "not found", 404); return
}

Expected AI output (abridged)

- %v drops the cause; errors.Is(err, sql.ErrNoRows) is false for callers.
- Logged and returned: the same failure appears twice in logs.
- Message starts with a capital and "Failed to"; becomes "get user 42: ..." style.
- The handler string-matches a driver message and depends on database/sql details.
Fix: export ErrNotFound from the store and translate sql.ErrNoRows at the boundary.

After

var ErrNotFound = errors.New("not found")

func (s *Store) GetUser(ctx context.Context, id int64) (*User, error) {
	var u User
	err := s.db.QueryRowContext(ctx, `SELECT id, email FROM users WHERE id = $1`, id).Scan(&u.ID, &u.Email)
	if errors.Is(err, sql.ErrNoRows) {
		return nil, fmt.Errorf("user %d: %w", id, ErrNotFound)
	}
	if err != nil {
		return nil, fmt.Errorf("get user %d: %w", id, err)
	}
	return &u, nil
}

// handler
u, err := store.GetUser(ctx, id)
switch {
case errors.Is(err, store.ErrNotFound):
	http.Error(w, "not found", http.StatusNotFound)
	return
case err != nil:
	logger.ErrorContext(ctx, "get user", "id", id, "err", err)
	http.Error(w, "internal error", http.StatusInternalServerError)
	return
}

Excerpt from card 17; the prompt itself is in the pack.

More Go prompt cards in the pack

Using these rules in Cursor, Claude Code and AGENTS.md tools

Cursor

Put _base.mdc and go.mdc in .cursor/rules/. Globs: go.mod, **/*.go.

Claude Code

Root CLAUDE.md, base first. Path-scoped variant .claude/rules/go.md for **/*.go, go.mod; PostToolUse hook: gofmt -w <file>.

AGENTS.md tools (Codex, Gemini CLI, Aider)

Base then stack in one AGENTS.md. Gemini CLI needs context.fileName; Aider needs --read AGENTS.md. Put this file in the directory with go.mod. Agents use the AGENTS.md nearest to the file being edited, so a nested copy overrides a root one for that subtree.

Related stacks

← All stacks