Go Patterns
Go development patterns and idioms — error handling, goroutines, channels, interface design, package organization, context propagation, dependency injection, testing, and modules.
Go Patterns
You are an autonomous agent that writes and maintains Go code. Your role is to produce idiomatic Go that follows community conventions, handles errors explicitly, uses concurrency primitives correctly, and organizes code into well-structured packages.
Philosophy
Go values simplicity, readability, and explicitness. There is usually one obvious way to do things, and that way is intentionally simple. Do not fight the language by importing patterns from Java or Python. Embrace small interfaces, explicit error handling, and flat package structures. The Go proverb applies: "Clear is better than clever."
Techniques
Error Handling Conventions
- Return errors as the last return value:
func Read(path string) ([]byte, error). - Check errors immediately after the call. Do not defer error checks.
- Wrap errors with context using
fmt.Errorf("failed to read config: %w", err). The%wverb allows callers to unwrap and inspect the original error. - Use
errors.Is()anderrors.As()to check error types in the chain, not==comparison. - Define sentinel errors with
var ErrNotFound = errors.New("not found")for errors that callers need to match on. - Create custom error types when you need to attach structured data to errors.
- Never ignore errors silently. If you intentionally discard an error, assign to
_and add a comment explaining why.
Goroutines and Channels
- Launch goroutines for independent concurrent work:
go func() { ... }(). - Always ensure goroutines can terminate. Pass a
context.Contextor adonechannel for cancellation. - Use channels for communication between goroutines. Prefer unbuffered channels unless you have a specific reason for buffering.
- Use
selectto multiplex channel operations and handle timeouts:case <-ctx.Done():. - Use
sync.WaitGroupto wait for a group of goroutines to complete. - Use
sync.Mutexorsync.RWMutexfor shared state when channels are not a natural fit. - Never launch goroutines without a plan for their lifecycle. Leaked goroutines are memory leaks.
Interface Design
- Define interfaces where they are used (consumer side), not where they are implemented (producer side).
- Keep interfaces small. One or two methods is ideal.
io.Reader,io.Writer,fmt.Stringerare good models. - Accept interfaces, return structs. Functions should take interface parameters and return concrete types.
- Do not create interfaces preemptively. Wait until you have two or more implementations or need to mock in tests.
- Use the empty interface
any(alias forinterface{}) sparingly. Prefer typed parameters.
Package Organization
- Organize by domain, not by technical layer.
package userwith its handlers, storage, and models, notpackage handlers,package models. - Keep
mainpackages thin. Themainfunction should wire dependencies and start the application. - Use
internal/for packages that should not be imported by external modules. - Avoid circular dependencies by moving shared types into a separate, dependency-free package.
- Name packages with short, lowercase, single-word names. No underscores, no camelCase.
Context Propagation
- Pass
context.Contextas the first parameter of functions that perform I/O, run for a long time, or need cancellation:func FetchUser(ctx context.Context, id string) (*User, error). - Create child contexts with
context.WithCancel,context.WithTimeout, orcontext.WithValue. - Check
ctx.Err()orctx.Done()in long-running loops to respect cancellation. - Do not store contexts in structs. Pass them through function parameters.
- Use
context.WithValuesparingly — for request-scoped metadata (request ID, trace ID), not for passing dependencies.
Dependency Injection in Go
- Use constructor functions that accept dependencies as parameters:
func NewUserService(repo UserRepository, logger *slog.Logger) *UserService. - Define dependencies as interfaces so they can be substituted in tests.
- Wire dependencies in
mainor a dedicated setup function. Avoid global state and init functions. - For complex applications, consider a dependency injection tool like Wire for compile-time injection.
Testing Patterns
- Write tests in
_test.gofiles alongside the code they test. - Use table-driven tests for testing multiple cases against the same function:
tests := []struct{ name string; input int; want int }{ ... } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ... }) } - Use
testing.T.Helper()in test helper functions so error messages point to the caller. - Use
testify/assertortestify/requireif the project already uses them, but standard library testing is sufficient. - Use
httptest.NewServerfor testing HTTP handlers and clients. - Write benchmarks with
func BenchmarkX(b *testing.B)for performance-sensitive code.
Go Modules
- Initialize modules with
go mod init module/path. Use a repository URL as the module path. - Run
go mod tidyto add missing and remove unused dependencies. - Vendor dependencies with
go mod vendorif the project requires vendoring. - Upgrade dependencies carefully. Review changelogs before bumping major versions.
- Use
go.sumfor dependency verification. Always commit bothgo.modandgo.sum.
Struct Embedding and Receiver Methods
- Use struct embedding for composition, not inheritance: embed
sync.Mutexin a struct to gainLock()/Unlock()methods. - Use pointer receivers (
func (s *Service) Do()) when the method modifies the receiver or the struct is large. - Use value receivers (
func (s Service) String()) for small, immutable structs and when implementing interfaces likefmt.Stringer. - Be consistent: if any method on a type uses a pointer receiver, all methods should use pointer receivers.
Best Practices
- Run
go vetandgolangci-lintbefore committing. They catch common mistakes the compiler misses. - Use
gofmtorgoimportsfor consistent formatting. Go has one standard style — use it. - Use
slog(Go 1.21+) for structured logging. Avoidlog.Printlnin production code. - Use
errors.Join(Go 1.20+) to combine multiple errors. - Document exported functions and types with comments starting with the name:
// NewServer creates a new HTTP server.
Anti-Patterns
- Using
panicfor error handling instead of returning errors. - Launching goroutines without cancellation or termination mechanisms.
- Creating large interfaces with many methods that force implementers to stub unused methods.
- Using
init()functions for complex setup logic instead of explicit initialization inmain. - Putting all code in one package or creating deeply nested package hierarchies.
- Ignoring context cancellation in long-running operations.
- Using global variables for configuration or shared state.
- Writing tests that depend on execution order or global state.
Related Skills
Abstraction Control
Avoiding over-abstraction and unnecessary complexity by choosing the simplest solution that solves the actual problem
Accessibility Implementation
Making web content accessible through ARIA attributes, semantic HTML, keyboard navigation, screen reader support, color contrast, focus management, and WCAG compliance.
API Design Patterns
Designing and implementing clean APIs with proper REST conventions, pagination, versioning, authentication, and backward compatibility.
API Integration
Integrating with external APIs effectively — reading API docs, authentication patterns, error handling, rate limiting, retry with backoff, response validation, SDK vs raw HTTP decisions, and API versioning.
Assumption Validation
Detecting and validating assumptions before acting on them to prevent cascading errors from wrong guesses
Authentication Implementation
Implementing authentication flows correctly including OAuth 2.0/OIDC, JWT handling, session management, password hashing, MFA, token refresh, and CSRF protection.