Effective Go Rules Checklist
Twenty-five distilled rules from Effective Go and community practice.
Search across all documentation pages
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.
gofmt every commit: Unformatted code never merges; gofmt -w is the only style tool.
test -z "$(gofmt -l .)" in CIgoimports for import hygiene: Missing and unused imports fail review the same as logic bugs.
Check every error return: No silent _ on errors except deliberate, documented cases.
if err != nil on every fallible callWrap with context using %w: Callers need errors.Is/errors.As through the stack.
fmt.Errorf("failed") without wrap when inspecting sentinelsHandle errors once: Log or return or translate to HTTP/gRPC status - not all three.
Use short names in small scopes: i, err, ctx are fine in loops; exported names stay descriptive.
Prefer := inside functions: Use var for package-level zero values and intentional type clarity.
Run go vet on every PR: Vet targets likely bugs, not preferences.
go vet ./... green in CIExport only stable surfaces: Uppercase identifiers are compatibility promises under semver.
Godoc every exported symbol: Comments start with the symbol name and state behavior.
go doc / pkg.go.dev renders complete package docsAccept interfaces, return structs: Parameters stay flexible; returns stay concrete unless wrapping is explicit.
func Load(r io.Reader) (*Config, error)Define interfaces at consumers: Producers return concrete types; callers declare minimal interfaces.
type Reader interface { Read... } in producer package "for extensibility"Prefer composition over embedding for behavior: Embed for forwarding; do not simulate inheritance hierarchies.
Make zero values useful when possible: Callers should not always need constructors for simple types.
Use pointer receivers when mutating: Value receivers for small immutable types; stay consistent per type.
Table-driven tests with subtests: Encode cases as data; name rows with t.Run.
Examples in example_test.go for libraries: Executable docs appear on pkg.go.dev.
cmd/ onlyInitialize maps before write: make(map[K]V) or literals; never assign to nil maps.
Assign append results: s = append(s, x) because capacity may reallocate.
make([]T, 0, n) when size knownCopy 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.
go test -racePass context as first parameter: Name it ctx; propagate to IO and RPC.
context.Context in structsDo not use panic for control flow: Panics are for programmer bugs and init failure, not expected errors.
Keep main packages thin: main wires dependencies; logic lives in libraries.
cmd/ + reusable packagesUse internal/ for non-public packages: Compiler-enforced boundaries beat comments alone.
internal/Tag modules with intentional semver: Breaking API changes bump major in v1+ via new module path or major version directory.
go mod tidy clean in CIEnough coverage for onboarding without duplicating specialized security and performance cheatsheets.
Promote repeated failures into team ADRs.
Formatting, some error checks (errcheck), receiver style (revive), and export comments.
Interface design and package boundaries remain human review.
Most Tier 1-2 rules apply.
Concurrency and stdlib subsets differ; scope checklists per build tag.
Each item maps to a section of Effective Go plus community operational defaults.
Read the explainer page first for layering context.
Document the exception with benchmarks in the PR.
Rules assume clarity first; hot paths may justify deviation.
Automate Tier 1.
Use Tier 2-3 as review flashcards during the first month.
Onboarding, quarterly audits, and after Go minor upgrades that add vet checks.
Yes - add to your ADR set.
Keep this page aligned with portable Go idioms, not org-only choices.
Use only for side-effect registration (database/sql drivers, image formats).
Document why in a comment; avoid blank imports for convenience.
Not directly.
Split when tests cannot cover branches or reviewers cannot follow control flow.
Prefer constrained type parameters over interface{} when capability is known.
Small interfaces remain valid at integration boundaries.
Run Code Quality Basics examples locally, then enable the same targets in CI.
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).
Reviewed by Chris St. John·Last updated Jul 16, 2026