Effective Go Rules Checklist
Twenty-five distilled rules from Effective Go and community practice.
Walk this list when onboarding, auditing a package, or turning repeated review comments into team policy.
How to Use This Checklist
- Complete Tier 1 before your first merge; Tier 2 before exporting library APIs; Tier 3 before shipping concurrent services.
- Automate Tier 1 in CI; keep Tier 2-3 in PR templates and reviewer cheatsheets.
- When a row fails, link the fix PR to the sibling page in Related for teaching.
- Revisit after Go minor upgrades; vet and stdlib idioms evolve slowly but do change.
Tier 1 - Formatting and errors (1-8)
-
gofmt every commit: Unformatted code never merges;
gofmt -wis the only style tool.- Verify:
test -z "$(gofmt -l .)"in CI
- Verify:
-
goimports for import hygiene: Missing and unused imports fail review the same as logic bugs.
- Verify: Editor on-save or pre-commit hook
-
Check every error return: No silent
_on errors except deliberate, documented cases.- Signal:
if err != nilon every fallible call
- Signal:
-
Wrap with context using
%w: Callers neederrors.Is/errors.Asthrough the stack.- Avoid:
fmt.Errorf("failed")without wrap when inspecting sentinels
- Avoid:
-
Handle errors once: Log or return or translate to HTTP/gRPC status - not all three.
- Discuss: Central middleware vs local logging policy
-
Use short names in small scopes:
i,err,ctxare fine in loops; exported names stay descriptive.- Rule: Scope length drives name length
-
Prefer := inside functions: Use
varfor package-level zero values and intentional type clarity.- Exception: When type differs from RHS
-
Run go vet on every PR: Vet targets likely bugs, not preferences.
- Verify:
go vet ./...green in CI
- Verify:
Tier 2 - APIs, types, and tests (9-17)
-
Export only stable surfaces: Uppercase identifiers are compatibility promises under semver.
- Ask: Would a rename break callers?
-
Godoc every exported symbol: Comments start with the symbol name and state behavior.
- Verify:
go doc/ pkg.go.dev renders complete package docs
- Verify:
-
Accept interfaces, return structs: Parameters stay flexible; returns stay concrete unless wrapping is explicit.
- Pattern:
func Load(r io.Reader) (*Config, error)
- Pattern:
-
Define interfaces at consumers: Producers return concrete types; callers declare minimal interfaces.
- Avoid:
type Reader interface { Read... }in producer package "for extensibility"
- Avoid:
-
Prefer composition over embedding for behavior: Embed for forwarding; do not simulate inheritance hierarchies.
- Signal: Promoted methods confuse readers
-
Make zero values useful when possible: Callers should not always need constructors for simple types.
- Counterexample: Mutexes and channels still need care
-
Use pointer receivers when mutating: Value receivers for small immutable types; stay consistent per type.
- Rule: One receiver style per exported type
-
Table-driven tests with subtests: Encode cases as data; name rows with
t.Run.- Verify: Failures identify the case row
-
Examples in
example_test.gofor libraries: Executable docs appear on pkg.go.dev.- Skip: When internal
cmd/only
- Skip: When internal
Tier 3 - Data, concurrency, modules (18-25)
-
Initialize maps before write:
make(map[K]V)or literals; never assign to nil maps.- Signal: panic: assignment to entry in nil map
-
Assign append results:
s = append(s, x)because capacity may reallocate.- Hot paths: Preallocate with
make([]T, 0, n)when size known
- Hot paths: Preallocate with
-
Copy loop variables when launching goroutines: Capture loop vars explicitly in closures when needed (older patterns) or rely on Go 1.22+ semantics and still test with
-race.- Verify:
go test -race
- Verify:
-
Pass context as first parameter: Name it
ctx; propagate to IO and RPC.- Never: Store
context.Contextin structs
- Never: Store
-
Do not use panic for control flow: Panics are for programmer bugs and
initfailure, not expected errors.- Libraries: Return errors to callers
-
Keep main packages thin:
mainwires dependencies; logic lives in libraries.- Layout:
cmd/+ reusable packages
- Layout:
-
Use internal/ for non-public packages: Compiler-enforced boundaries beat comments alone.
- Verify: Consumers cannot import
internal/
- Verify: Consumers cannot import
-
Tag modules with intentional semver: Breaking API changes bump major in v1+ via new module path or major version directory.
- Tooling:
go mod tidyclean in CI
- Tooling:
Applying the Checklist in Order
- Tier 1 (1-8): Low reversibility cost - fix before feature work continues.
- Tier 2 (9-17): Affects API compatibility - discuss before widening exports.
- Tier 3 (18-25): Touches concurrency and distribution - pair with Concurrency Rules Checklist and Module & Dependency Rules.
FAQs
Why exactly twenty-five items?
Enough coverage for onboarding without duplicating specialized security and performance cheatsheets.
Promote repeated failures into team ADRs.
Which items can golangci-lint enforce?
Formatting, some error checks (errcheck), receiver style (revive), and export comments.
Interface design and package boundaries remain human review.
Do these rules apply to TinyGo or WASM?
Most Tier 1-2 rules apply.
Concurrency and stdlib subsets differ; scope checklists per build tag.
How does this relate to Effective Go the document?
Each item maps to a section of Effective Go plus community operational defaults.
Read the explainer page first for layering context.
What if a rule conflicts with measured performance?
Document the exception with benchmarks in the PR.
Rules assume clarity first; hot paths may justify deviation.
Should juniors memorize all twenty-five?
Automate Tier 1.
Use Tier 2-3 as review flashcards during the first month.
How often should teams re-walk the checklist?
Onboarding, quarterly audits, and after Go minor upgrades that add vet checks.
Can we add a twenty-sixth team rule?
Yes - add to your ADR set.
Keep this page aligned with portable Go idioms, not org-only choices.
Where do blank imports fit?
Use only for side-effect registration (database/sql drivers, image formats).
Document why in a comment; avoid blank imports for convenience.
Are long functions a checklist violation?
Not directly.
Split when tests cannot cover branches or reviewers cannot follow control flow.
How do generics change interface rules?
Prefer constrained type parameters over interface{} when capability is known.
Small interfaces remain valid at integration boundaries.
What is the fastest path through Tier 1?
Run Code Quality Basics examples locally, then enable the same targets in CI.
Related
- Effective Go as a Living Standard - conceptual layering
- Go Rules Quickstart - runnable intro
- API Design Rules for Go Libraries - export surface rules
- Code Review Standards for Go Teams - reviewer depth
- Go Rules & Best Practices Summary - section-wide index
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).