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