These instructions are for AI assistants working in this project.
Always open @/openspec/AGENTS.md when the request:
- Mentions planning or proposals (words like proposal, spec, change, plan)
- Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work
- Sounds ambiguous and you need the authoritative spec before coding
Use @/openspec/AGENTS.md to learn:
- How to create and apply change proposals
- Spec format and conventions
- Project structure and guidelines
Keep this managed block so 'openspec update' can refresh the instructions.
This document provides guidelines for AI agents working with this Go repository. Following these practices ensures consistent, high-quality code generation and maintenance.
- Follow the Standard Go Project Layout
- Place application code in
/cmdfor executables - Store reusable library code in
/pkg - Keep internal packages in
/internal(not importable by external projects) - Use
/apifor API definitions (OpenAPI/Swagger, Protocol Buffers, etc.) - Place build scripts and configurations in
/build - Store deployment configurations in
/deployments
- Always use Go modules (
go.modandgo.sum) - Run
go mod tidyafter adding or removing dependencies - Use semantic versioning for module versions
- Prefer explicit dependency versions over
latest
- All code must be formatted with
gofmtorgoimports - Use
goimportsto automatically manage import statements - Run
go vetto catch common errors - Use
golangci-lintfor comprehensive linting
- Use
camelCasefor unexported identifiers - Use
PascalCasefor exported identifiers - Keep names concise but meaningful (prefer
userCountovernumberOfUsers) - Use single-letter names only for short-lived variables (loop counters, receivers)
- Name interfaces with
-ersuffix for single-method interfaces (e.g.,Reader,Writer) - Avoid stuttering in names (prefer
user.Nameoveruser.UserNamein packageuser)
- Keep package names lowercase, single-word when possible
- Package names should be descriptive of their purpose
- Avoid generic names like
util,common, orhelpers - One package per directory
- Always check and handle errors explicitly
- Use
errors.New()orfmt.Errorf()for simple errors - Wrap errors with context using
fmt.Errorf("context: %w", err)(Go 1.13+) - Create custom error types for errors that need special handling
- Return errors as the last return value
- Don't panic except in truly exceptional circumstances (initialization failures)
- Start error messages with lowercase (they're often wrapped)
- Don't end error messages with punctuation
- Provide sufficient context in error messages
- Include relevant variable values in error messages
- Place tests in the same package as the code (use
_test.gosuffix) - Use one test file per source
_.gofile. Do not split tests into multiple files for a single source file. - Use test suites (use library "testify/suite") to set up tests and break them down.
- Use
package <name>_testfor black-box testing - Name test functions
TestXxxwhere Xxx describes what's being tested - Use table-driven tests for testing multiple scenarios
- Use subtests (
t.Run()) to organize related test cases - for mocks, use the mock package from "github.com/stretchr/testify". Mocks should go in the same directory as the tests in a file called "mocks_test.go".
- Aim for high test coverage (80%+ for critical paths)
- Test both happy paths and error cases
- Use
testing.Tfor unit tests,testing.Bfor benchmarks - Mock external dependencies using interfaces
- Write meaningful test failure messages
- Use
testifyor similar libraries for assertions when appropriate - Use test suites
- Benchmark performance-critical code
- Include example functions for documentation (
Exampleprefix) - Examples should have
// Output:comments for verification
- Use goroutines for concurrent operations, not parallel loops by default
- Always ensure goroutines can exit (avoid leaks)
- Use
context.Contextfor cancellation and timeouts - Prefer channels for communication, mutexes for state protection
- Close channels only from the sender side
- Use buffered channels judiciously (understand backpressure)
- Use
sync.WaitGroupto wait for multiple goroutines - Use
sync.Oncefor one-time initialization - Prefer
sync.RWMutexwhen reads significantly outnumber writes - Document which goroutine owns which data
- Use the race detector (
go test -race) during development
- Write godoc-style comments for all exported identifiers
- Start comments with the name of the element being described
- Use complete sentences in documentation comments
- Explain "why" in comments, not "what" (code should be self-explanatory)
- Keep comments up-to-date with code changes
- Include a package comment in one file (typically
doc.gofor complex packages) - Provide usage examples in package documentation
- Document any non-obvious behavior or limitations
- Profile before optimizing (
pprof, benchmarks) - Avoid premature optimization
- Use appropriate data structures (maps, slices, arrays)
- Pre-allocate slices when size is known:
make([]Type, 0, capacity) - Reuse buffers with
sync.Poolfor frequently allocated objects - Be mindful of memory allocations in hot paths
- Always close resources (
defer file.Close()) - Use
deferfor cleanup operations - Place
deferimmediately after successful resource acquisition - Be aware of
deferin loops (may want to use a closure) - Use
context.Contextfor request-scoped values and cancellation
- Minimize external dependencies
- Vet dependencies for maintenance and security
- Pin dependency versions in
go.mod - Regularly update dependencies (
go get -u) - Use
go mod verifyto check dependency integrity
- Validate and sanitize all inputs
- Use prepared statements for database queries
- Don't log sensitive information
- Use
crypto/randfor cryptographic operations, notmath/rand - Keep secrets in environment variables or secure vaults, never in code
- Run security scanners (
gosec,nancy) in CI/CD
- Keep interfaces small (single method when possible)
- Accept interfaces, return concrete types
- Define interfaces where they're used, not where they're implemented
- Use
io.Readerandio.Writerwhen applicable
- Keep functions small and focused on a single responsibility
- Limit function parameters (consider a config struct for many parameters)
- Use functional options pattern for optional parameters
- Return early to reduce nesting
- Use environment variables for deployment-specific configuration
- Support configuration files (YAML, JSON, TOML) for complex setups
- Provide sensible defaults
- Validate configuration at startup
- Use libraries like
https://github.com/goschtaltfor complex configuration - Use libraries like
alecthomas/kongfor parsing command line arguments
- Use an observer pattern to log events so the code is not tied to a specific logger. Components should notify a log listeners to log events.
- Use structured logging (
slog) - Log at appropriate levels (DEBUG, INFO, WARN, ERROR)
- Include context in log messages (request IDs, user IDs)
- Don't log sensitive information
- Make logs machine-readable and grep-friendly
- Use
makeor task runners for build automation - Support cross-compilation when needed
- Use build tags for platform-specific code
- Include version information in binaries (
-ldflags) - Create reproducible builds
- Use multi-stage builds to minimize image size
- Run as non-root user in containers
- Use scratch or distroless base images for production
- Copy only necessary files into the image
- Use
.dockerignoreto exclude unnecessary files
- Run tests on every commit
- Include linting and formatting checks
- Run the race detector in CI
- Check test coverage
- Validate
go.modandgo.sumconsistency - Build for all target platforms
- Enforce minimum test coverage thresholds
- Require all linters to pass
- Run security scanners
- Validate documentation completeness
- Write clear, descriptive commit messages
- Keep commits atomic and focused
- Reference issue numbers in commit messages
- Use conventional commits format when applicable
- Use feature branches for development
- Keep the main branch stable and deployable
- Squash commits when merging if appropriate
- Delete merged branches
type Config struct {
timeout time.Duration
retries int
}
type Option func(*Config)
func WithTimeout(d time.Duration) Option {
return func(c *Config) { c.timeout = d }
}
func NewService(opts ...Option) *Service {
cfg := &Config{timeout: 30 * time.Second, retries: 3}
for _, opt := range opts {
opt(cfg)
}
return &Service{config: cfg}
}Use for complex object construction with validation
Always pass context.Context as the first parameter to functions that may block or need cancellation
go fmt/goimports- formattinggo vet- static analysisgolangci-lint- comprehensive lintinggo test -race- race detectiongo test -cover- coverage analysispprof- profilinggosec- security scanningstaticcheck- additional static analysis
- Configure editor to run
goimportson save - Enable inline error checking
- Set up debugging support
- Install Go extension/plugin for your editor
- Design types so their zero value is useful
- Document when zero value is not ready to use
- Use struct embedding for composition
- Understand promoted methods and fields
- Avoid embedding for "has-a" relationships
- Be explicit with type conversions
- Use type aliases for clarity, not just brevity
- Use pointers for large structs to avoid copies
- Use pointers when you need to modify the receiver
- Don't use pointers for performance unless profiling shows it matters
- Don't use
init()functions except when absolutely necessary - Avoid global variables and package-level state
- Don't ignore errors (use
_sparingly and only when justified) - Don't use panic/recover for normal error handling
- Avoid reflection unless absolutely necessary
- Don't export unnecessarily (keep APIs minimal)
- Avoid premature abstraction
- Don't use channels when a mutex would suffice
- Avoid context.Value for anything except request-scoped data
This document should be regularly updated as the project evolves and new best practices emerge.