Go Philosophy Best Practices
Ten principles that keep Go codebases aligned with the language's ethos: readable, explicit, and operable at scale.
How to Use This List
- Treat items as team norms, not one-off review nitpicks - cite them in style guides and ADRs.
- Check new packages and services against sections A–C before merging.
- Revisit quarterly as Go releases add idioms (
go fixmodernizers in 1.26 are a good trigger). - Pair with linters (
golangci-lint,go vet) so philosophy becomes CI signal, not memory.
A - Simplicity and Readability
- Let
gofmtown formatting. No custom alignment debates in review; usegofmt/goimportsin CI. - Keep packages focused on one job. Split when godoc reads like a junk drawer; prefer
checkoutoverutil. - Name by purpose, not pattern.
UserStorebeatsUserDAO; Go is not Java enterprise cosplay. - Write godoc for every exported symbol. Future readers discover contracts through pkg.go.dev and IDE hovers.
- Prefer clear code over clever abstractions. If a junior engineer cannot trace control flow in one pass, simplify.
B - APIs and Dependencies
- Accept interfaces, return structs. Interfaces stay small at consumer sites; constructors return concrete types.
- Pass
context.Contextas the first parameter on I/O boundaries. Never store context in structs. - Return
(T, error)and wrap with%w. Document sentinel errors withvar ErrFoo = errors.New("...")for stableerrors.Is. - Inject dependencies through
Newinmain. Avoid globalvar db *sql.DBin libraries; wire explicitly incmd/. - Minimize exported surface area. Unexport helpers until a second package genuinely needs them.
C - Concurrency and Operations
- Know how every goroutine exits. Time-bound workers, respect
ctx.Done(), and avoid unbounded fan-out. - Run
go test -racein CI on packages that use goroutines. Philosophy without detection is wishful thinking. - Use the standard library before importing frameworks.
net/http,encoding/json, andlog/slogcover many services. - Profile before optimizing (
pprof, execution tracer). Measure aligns with Go culture; guessing does not. - Ship static binaries with explicit graceful shutdown. Handle
SIGTERM, drain handlers, and align with Kubernetes probe semantics.
D - Toolchain and Evolution
- Stay current on supported Go releases. Pick up runtime wins (Green Tea GC default in 1.26) and security fixes.
- Run
go fixmodernizers during upgrades. Let tooling migrate idioms instead of manual churn across monorepos. - Pin modules and scan with
govulncheck. Philosophy includes supply-chain hygiene, not only syntax. - Treat the Go 1 compatibility promise as a team asset. Avoid breaking exported APIs without a
/v2module path plan.
Applying Philosophy in Code Review
- Blocking: race-prone concurrency, missing context on RPC/HTTP handlers, exported interfaces that force wide mocks.
- Discuss: package splits, error taxonomy changes, new third-party frameworks duplicating stdlib.
- Defer: micro-optimizations without profiles, renaming for aesthetic taste when behavior is stable.
FAQs
Why only ten principles in the title but more checkboxes?
The title reflects the core cultural decalogue; sections group related enforceable rules.
Adopt the whole list even though marketing rounds to ten themes.
Which items should be CI-enforced vs culture-enforced?
Enforce gofmt, go vet, golangci-lint, -race, and govulncheck in CI.
Package boundaries and API shape stay human review with ADR backup.
How do these practices interact with frameworks like gin or gRPC?
Frameworks are fine when they remove boilerplate without hiding lifecycle.
Philosophy still demands explicit wiring, context, and shutdown in main.
Should internal services use the same rules as public modules?
Yes for concurrency, context, and errors.
Internal code can move faster on breaking changes - but prefer clear module boundaries anyway.
What is the top mistake teams make after choosing Go?
Importing Java/C# enterprise patterns - giant DI graphs, wide interfaces, and stringly errors - which Go's toolchain does not reward.
How do generics fit these practices?
Use generics to remove duplication, not to build generic frameworks.
If func Map is clearer without type parameters, skip them.
When is breaking the "stdlib first" rule justified?
When frameworks provide tested middleware ecosystems (auth, binding, OpenTelemetry hooks) that would take quarters to replicate.
Document the dependency in an ADR.
How often should teams re-read philosophy docs?
Onboard every engineer, revisit during major Go upgrades, and after incidents caused by concurrency or API drift.
Do these practices apply to TinyGo or WASM targets?
Concurrency and context rules still apply where the runtime supports them.
Memory and syscall constraints may require slimmer dependencies - verify board targets at build.
How do philosophy practices map to Kubernetes operators?
Explicit reconcile loops, context-aware client calls, structured logging, and leader election shutdown mirror service rules.
Use controller-runtime patterns instead of inventing watch glue.
What metrics prove philosophy is working?
Lower incident rates from panics/races, faster onboarding surveys, fewer breaking API changes, and upgrade PRs dominated by tooling (go fix) not manual edits.
Where do I go for language mechanics after philosophy?
Move to fundamentals, concurrency, and architecture sections once team norms are aligned.
Related
- Why Go Exists - principles behind the checklist
- Simplicity as a Deliberate Constraint in API Design - API-level detail
- Go Philosophy Basics - runnable examples of idioms
- When Go Is the Wrong Tool - when not to force idiomatic Go
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).