$ npx rulesync-cli pull✓ Wrote CLAUDE.md (2 rulesets)# Coding Standards- Always use async/await- Prefer named exports
Rule Writing

AI Coding Rules for Go Teams

Go has strong conventions, but AI assistants still get error handling, interfaces, and concurrency wrong. These rules fix the most common issues.

8 min read·January 30, 2025

Go's conventions are strong — but AI assistants still need explicit rules

Error wrapping, small interfaces, context propagation, and idiomatic testing

Why Go Needs Specific AI Rules

Go is famously opinionated — gofmt handles formatting, and the community has strong conventions for naming, error handling, and package design. You might think AI assistants would generate idiomatic Go by default. They don't.

The most common AI failures in Go: returning bare errors without wrapping (losing the call chain context), creating overly complex interfaces (Java-style, not Go-style), using panic for recoverable errors, spawning goroutines without lifecycle management, and ignoring context propagation. Each pattern is idiomatic in another language — the AI is generating Java or Python error handling in Go syntax.

Go's explicit error handling is where AI assistants struggle most. Every language has error handling, but Go's approach — returning errors as values, wrapping them for context, checking them at every call site — is unique enough that AI models trained mostly on Python and JavaScript consistently get it wrong.

Rule 1: Idiomatic Error Handling

The cardinal sin of AI-generated Go: returning `err` without wrapping it. The AI generates `return err` at every call site, which means when an error surfaces at the top of the stack, you have no idea where it originated. The error message says 'connection refused' but you don't know if it was the database, the cache, or the external API.

The rule: 'Always wrap errors with fmt.Errorf and %w: return fmt.Errorf("getting user %s: %w", id, err). Never return bare err. The wrapping message should describe what the current function was trying to do when the error occurred. Use errors.Is and errors.As for error checking, never string comparison.'

For sentinel errors, add: 'Define package-level error variables with errors.New for errors that callers need to check: var ErrNotFound = errors.New("not found"). Never use string-based error checks.'

⚠️ Cardinal Sin

Returning bare `err` without wrapping loses the entire call chain context. One rule — 'Always wrap with fmt.Errorf and %w' — transforms Go error debugging from guesswork to traceable chains.

Rule 2: Small Interfaces

AI assistants trained on Java and C# code generate Go interfaces with 10-15 methods. This is anti-idiomatic in Go, where the convention is small, focused interfaces — often just 1-3 methods. The io.Reader interface has one method. The fmt.Stringer interface has one method. Your interfaces should be similarly focused.

The rule: 'Define interfaces with 1-3 methods maximum. Accept interfaces, return structs. Define interfaces at the point of use (in the consuming package), not the implementing package. Never create interfaces for a single implementation — only introduce an interface when you have or expect multiple implementations or need to mock in tests.'

This rule prevents a pattern the AI loves: creating a UserService interface with 15 methods, then a UserServiceImpl struct that implements it. In Go, you'd just have a UserService struct with methods. The interface, if needed, would be defined by the consumer with only the methods it uses.

💡 Go Idiom

Accept interfaces, return structs. Define interfaces at the point of use with 1-3 methods. If you only have one implementation, you don't need an interface — the AI loves creating unnecessary abstractions.

Rule 3: Context Propagation and Goroutine Safety

AI assistants frequently generate Go code that ignores context.Context — the primary mechanism for cancellation, timeouts, and request-scoped values. Functions that do I/O without accepting a context can't be cancelled, leading to goroutine leaks and unresponsive servers.

The rule: 'Every function that performs I/O (database queries, HTTP requests, file operations) must accept context.Context as its first parameter. Pass ctx through the entire call chain from the HTTP handler to the database query. Never create a background context (context.Background()) inside a request handler — always use the request context.'

For goroutines: 'Every goroutine must be cancellable via context. Use errgroup.Group for managing groups of goroutines. Never spawn a goroutine without a mechanism to signal it to stop. Always handle the done channel: select { case <-ctx.Done(): return ctx.Err() }.'

Rule 4: Table-Driven Tests

AI assistants generate Go tests as individual functions: TestAdd_Positive, TestAdd_Negative, TestAdd_Zero. Idiomatic Go uses table-driven tests with subtests — one test function with a slice of test cases, each run through t.Run. This pattern is more maintainable, easier to extend, and produces better failure output.

The rule: 'Write table-driven tests using a []struct with test name, inputs, expected outputs, and optional error expectations. Use t.Run(tc.name, func(t *testing.T) { ... }) for each case. Use t.Helper() in test helper functions. Use testify/assert for assertions, or plain if-statements with t.Errorf — never t.Fatal in subtests.'

For integration tests: 'Use t.Cleanup for teardown instead of defer. Use test fixtures in testdata/ directories. Use build tags to separate unit and integration tests: //go:build integration.'

ℹ️ Testing Pattern

Table-driven tests with t.Run subtests are the Go standard. One test function with a slice of cases is more maintainable than 10 separate TestFoo functions — and produces better failure output.

Rule 5: Package and Project Structure

The AI's default Go project structure is often flat (everything in main) or over-abstracted (deep package hierarchies like Java). Idiomatic Go prefers a flat-ish structure with domain-oriented packages that have clear, single-word names.

The rule: 'Follow the Standard Go Project Layout for larger projects. Use single-word package names (user, not userservice). No generic packages named utils, helpers, or common — put functions in the package where they're used. The cmd/ directory contains main packages. The internal/ directory contains private packages. The pkg/ directory (if used) contains public library code.'

For APIs: 'HTTP handlers go in handler/ or api/ package. Business logic goes in the domain package (user/, order/, payment/). Database access goes in a store/ or repository/ package. Each package exposes an API through exported functions and types — never import from handler into domain.'

Complete Go Rules Template

Here's a consolidated template for Go teams. It covers the five core areas where AI output most frequently deviates from idiomatic Go. Customize for your specific project structure and libraries.

  • Error handling: Always wrap with fmt.Errorf("%w"), never return bare err
  • Interfaces: 1-3 methods max, accept interfaces return structs, define at point of use
  • Context: First parameter in all I/O functions, pass through entire chain, never background in handlers
  • Goroutines: Always cancellable via context, use errgroup for groups, handle ctx.Done()
  • Testing: Table-driven with t.Run subtests, t.Helper in helpers, testify/assert for assertions
  • No panic: Return errors for recoverable failures, panic only for programmer errors (invariant violations)
  • Naming: MixedCaps, single-word packages, no get/set prefixes, unexported by default
  • Imports: Group stdlib, external, internal with blank lines between groups