go-style-guide
Go (Golang) Style Guide Skill
This skill defines practical Go engineering conventions optimized for:
- humans reading and maintaining code
- coding agents generating or refactoring code reliably
- production readiness (correctness, testability, performance)
Use this skill whenever you are working with Go: new code, refactors, reviews, and architecture decisions.
TL;DR
- Design for testability first; inject dependencies and keep logic pure.
- Prefer
Configin → concrete struct out; validate, default, and document important runtime knobs. - Errors are contracts: use sentinels for durable branching; wrap the rest with
%worerrors.Join. - Keep packages reusable: no hidden globals, no default logging, no surprise side effects.
- Coverage is a signal, not proof; test edge cases and misuse paths, not just happy paths.
- Follow "accept interfaces, return structs"; consumers usually define interfaces, shared contract packages are a special case.
- Keep
main.gothin; follow existing repo layout conventions rather than forcing one directory shape. - Benchmark hot paths before claiming wins, and run concurrency code with
-race. - Maintain contracts such as function signatures, config shape, error behavior, and doc comments; they are as important as the code itself.
House Style Disclaimer
This is intentionally opinionated. It favors consistency and long-term maintainability over accommodating every Go style preference.
Compatibility
Examples assume modern Go and use standard-library features such as
errors.Join and log/slog. Apply the guide within the constraints of the
target repository's supported Go version.
Execution Protocol
Follow this workflow when using the skill for implementation work:
-
Inspect the repository first. Read existing package layout, constructors, tests, and error conventions before proposing new APIs or moving files.
-
Define the contract before coding. Decide the package boundary, config shape, concrete return type, sentinel errors, and context or shutdown expectations up front.
-
Write or update tests early when practical. Start with table-driven unit tests, add fuzz tests for parsing or other input-heavy code, and add benchmarks for performance-sensitive paths.
-
Implement the smallest maintainable change. Follow the repository's existing layout, keep
main.gothin, and avoid introducing new abstractions without a clear boundary. -
Run the finishing checks. Format with
gofmt(andgoimportsif the repo uses it), run the relevantgo testtargets, rungo test -racewhen concurrency is involved, and run benchmarks when claiming performance improvements. -
Verify the human-facing contract. Make sure docs, comments, config defaults, and error behavior match the code you are shipping.
Quick Rules Table
| Topic | Rule | Reference |
|---|---|---|
| Testability | Design for confidence, not coverage percentages; test edge and misuse cases | references/TESTING.md |
| Constructors | Config in → concrete struct out; validate + default in New; use Config.Validate() when config logic grows |
references/CONFIG.md |
| Errors | Use sentinels for durable branching; wrap with %w or errors.Join; keep recover at app boundaries |
references/ERRORS.md |
| Logging | Packages do not log by default; hot-path logging is a performance decision | references/LOGGING.md |
| Interfaces | "Accept interfaces, return structs"; consumers usually define interfaces | references/INTERFACES.md |
| Documentation | Write idiomatic godoc and durable comments; never add agent-context comments | references/DOCUMENTATION.md |
| Layout | Keep packages shallow, avoid junk drawers, and follow repo conventions | references/LAYOUT.md |
| Entry Points | main.go is wiring only |
references/LAYOUT.md |
| Benchmarks | Benchmark hot paths; use b.ReportAllocs() and compare runs with benchstat |
references/BENCHMARKS.md |
| Testing | Table-driven, stdlib-first, defensive against misuse, and fuzz-heavy where inputs are complex | references/TESTING.md |
| Concurrency | Every goroutine needs a shutdown path; use context.Context, -race, and jitter where needed |
references/CONCURRENCY.md |
| Reviews | Use the checklist when reviewing Go changes | references/REVIEW-CHECKLIST.md |
Common Pitfalls
- Returning interfaces by default instead of concrete types.
- Treating coverage percentages as proof of correctness.
- Logging in reusable packages instead of returning errors.
- Passing global app config through packages rather than local
Config. - Leaving critical runtime knobs on dangerous defaults.
- Forcing a house directory layout onto repos that already have clear conventions.
- Loading every reference document before you know which topic the task touches.
- Shipping changes that claim performance wins without benchmarks or concurrency safety without
-race.
Core Principles
Testability is first-class
- Prefer designs that are easy to test without booting an entire application.
- Inject dependencies explicitly.
- Keep pure logic isolated.
- Test edge cases, invalid inputs, and misuse paths rather than only happy paths.
Config-driven construction
- Prefer
Configin → struct out constructors. - Validate at construction.
- Default explicitly.
- Make important runtime knobs visible and documented.
Errors are a contract
- Prefer sentinel errors for durable conditions callers need to branch on.
- Use
%w(orerrors.Join) so callers can useerrors.Is/As. - Prefer a small set of durable meanings plus contextual wrapping.
Benchmark what matters
- Add benchmarks for performance-sensitive code paths.
- Avoid "it's faster" claims without
go test -bench.
Packages are reusable by default
- Keep packages domain-focused and individually testable.
- Avoid global state and hidden side effects.
Structure should reinforce intent
- Use package boundaries and entry points as architectural guardrails.
- Follow established repo conventions when they are clear.
Package Types
App package (orchestrator)
Owns:
- dependency wiring (DB, clients, loggers)
- lifecycle (start/stop)
- error policy (retry, ignore, crash)
- logging and metrics policy
Non-app packages (reusable units)
Rules:
- No direct logging (see
references/LOGGING.md) - Return errors, don't hide them
- Define a local
Config/Optscontract - Accept initialized dependencies (DB/client/etc); do not create them internally
Directory Structure
Services / apps
cmd/<appname>/main.gofor entrypoints- Keep
main.gothin: parse config, wire dependencies, call the app entrypoint.
This guide often prefers service repos that group packages under pkg/, with
orchestration in something like pkg/app, but that is a house preference, not
Go law.
Example:
cmd/myapp/main.go
pkg/...
pkg/app/...
If a repository already uses top-level packages, internal/, or a mixed shape
with clear rules, follow the repository convention instead of forcing pkg/.
Do not restructure an existing repository to introduce pkg/ unless you were
explicitly asked to do that migration.
Libraries
-
Packages at top-level directories, not nested under
pkg/orinternal/. -
Avoid junk drawers (
utils,common) unless they truly represent a domain.
Constructors and Config
New(cfg Config) (*T, error)orDial(cfg Config) (*T, error)- Prefer passing
Configby value; validate + default inside constructor - For complex configs, move non-trivial validation into
func (c *Config) Validate() error - Return a concrete type by default
- Treat constructor-normalized config as immutable internal state
- Config is owned by the package, not the app
- Avoid
init()for normal construction; it usually hides globals, ordering dependencies, or side effects better handled by explicit setup - Follow "accept interfaces, return structs": inject boundary interfaces and return concrete types unless there is a clear multi-implementation boundary
- Make important runtime knobs explicit: timeouts, pool sizes, lifetimes, backoff/retry ceilings, and similar operational settings
- Use explicit runtime controls for degraded modes in critical services; do not
expect callers to mutate
Configafter construction
See: references/CONFIG.md, references/INTERFACES.md
Logging
Logging is owned by the application.
If a package must log (rare async/network/runtime cases), inject
*slog.Logger via Config, default to discard, keep structured logs, and
prefer context-aware log methods when a real context.Context is already
available.
Treat hot-path logging as a performance decision. Avoid chatty request-path
Info logs, and if async logging is used, document buffering, backpressure,
and drop behavior.
See: references/LOGGING.md
Errors
- Export
var ErrX = errors.New("...")for stable, durable meanings callers may need to branch on - Wrap with
%wor useerrors.Joinsoerrors.Isworks - Don't use
%sto wrap errors (it breaks unwrap semantics) - Prefer a small set of sentinels plus contextual wrapping rather than a new sentinel for every failure path
- Keep
recoverat application boundaries or middleware, not in reusable packages - Packages return errors; apps log them and decide whether to ignore, retry, or crash
See: references/ERRORS.md
Testing
- Prefer table-driven tests with clear, behavior-oriented case names.
- Coverage is a signal, not proof; confidence comes from meaningful assertions and edge cases.
- Test defensive behavior, not just the current happy path.
- Use
go test -fuzzfor parsers, decoders, and other input-heavy code. - Use
TestMainonly for true package-wide lifecycle setup/teardown. - Run
-racein CI for concurrency-sensitive code.
See: references/TESTING.md
Documentation + Comments
- Public packages need package docs explaining purpose and main usage, using
/** ... */package comments. - Exported identifiers get idiomatic doc comments, helpful links like
[Config], and executableExample_docs when teaching usage matters. Functions, methods, types, vars, consts, and fields use//doc comments. - Comments explain why, contract, or intent; never narrate obvious code.
- No agent-context comments or self-referential TODO/FIXME notes.
See: references/DOCUMENTATION.md
Concurrency
- Every goroutine must have a clear shutdown path via
context.Context. - Long-lived services implement
Close()orStop()with concurrency synchronization to ensure graceful shutdown. - Use
sync.Mutexfor complex state,sync/atomicfor simple counters and flags. - Prefer
selectwithctx.Done()for blocking operations. - Use context-aware I/O APIs such as
QueryContextandNewRequestWithContext. - Use
deferfor cleanup in the scope that acquires a resource. - Add jitter to recurring background work when synchronized schedules would create spikes.
- Graceful shutdown should fail readiness, drain in-flight work, stop listeners, and wait for tracked work to finish.
- If it's not tested with
-race, assume it's not concurrency-safe.
See: references/CONCURRENCY.md
File Organization and Efficiency
Files follow idiomatic ordering: package docs, imports, consts/vars, sentinel
errors, types, constructors, exported methods, unexported helpers. Avoid
catch-all files like types.go or util.go.
Keep hot-path structs compact (field ordering for padding) but do not micro-optimize without benchmarks.
Use structure as an architectural guardrail. Package boundaries and entry points should make the intended placement of new behavior obvious.
Avoid init() in general. It is often a sign of hidden globals, implicit
registration, or startup side effects that should be made explicit.
See: references/LAYOUT.md
Benchmarks
Add benchmarks for hot-path functions, serialization, concurrency primitives,
and adapters in tight loops. Use b.ReportAllocs(), include realistic inputs,
and compare alternatives when proposing changes.
Let the runner control b.N, run important numbers on a quiet machine, and use
benchstat when comparing benchmark results.
See: references/BENCHMARKS.md
Interfaces + Implementations
Default to small consumer-defined interfaces and producer-owned concrete structs.
Use shared contract packages with subpackages (drivers/, backends/, etc.)
as a special case when the package's primary purpose is to define a common
boundary across multiple implementations.
See: references/INTERFACES.md
Reference Index
Use these supporting documents when deeper detail is needed:
-
references/LOGGING.md Logging rules: default no-logging-in-packages guidance, exceptions,
sloginjection, and hot-path cost guidance. -
references/ERRORS.md Durable error contracts, wrapping rules, and
errors.Is/Asguidance. -
references/CONFIG.md Canonical
Configstruct patterns, constructor validation + defaults, and operational controls. -
references/INTERFACES.md Interface boundaries, consumer-defined defaults, and special-case driver patterns.
-
references/DOCUMENTATION.md Package docs, idiomatic godoc, internal function comments, field docs, and durable comment rules.
-
references/LAYOUT.md File organization, struct field efficiency, package naming guidance, and architectural guardrails.
-
references/BENCHMARKS.md Benchmark expectations, templates, and result-comparison rules.
-
references/TESTING.md Stdlib-first testing patterns, table-driven tests, helpers, defensive testing, and test file conventions.
-
references/CONCURRENCY.md Goroutine lifecycle, context propagation, graceful shutdown,
-race, jitter, and synchronization patterns. -
references/REVIEW-CHECKLIST.md PR review rubric for humans and coding agents.