Go Fundamentals Best Practices
Core habits every Go developer should internalize first.
These rules reinforce the fundamentals section: clear names, explicit error handling at boundaries, and collections used with their real semantics.
How to Use This List
- Apply during code review and when learning a new repo - not as a one-time checklist.
- Prefer
gofmt,go vet, andgolangci-lintto catch violations automatically. - When a rule conflicts with measured performance data, document the exception in a comment or ADR.
- Revisit after major Go upgrades; toolchain and stdlib idioms evolve slowly but do change.
A - Naming and declarations
- Use short, meaningful names in small scopes and longer names at package scope. Locals like
ianderrare fine in loops; exported APIs deserve descriptive names. - Prefer
:=inside functions when the type is obvious. Reservevarfor package-level declarations and intentional zero values. - Export only what callers need. Uppercase identifiers are a compatibility promise - keep exported surfaces small.
- Group related constants with
iota. Avoid magic numbers scattered in logic; name states and flags explicitly. - Avoid package-level mutable variables unless synchronized. Pass dependencies via constructors and function parameters instead of hidden globals.
B - Types, zero values, and collections
- Design structs so zero values are useful when possible. Callers should not always need a constructor for simple types.
- Initialize maps with
makeor literals before writes. Never assign to a nil map. - Treat
len(string)as byte length, not character count. Userangeorutf8.RuneCountInStringfor user-visible text. - Preallocate slices when size is known.
make([]T, 0, n)reduces allocations in hot loops. - Assign the result of
append.s = append(s, x)becauseappendmay return a new header.
C - Pointers, structs, and APIs
- Use pointer receivers when methods mutate state. Keep value receivers for small immutable types unless consistency demands pointers.
- Pass pointers for large structs only after profiling or clear mutation needs. Default to values for small records.
- Check nil pointers before dereference at API boundaries. Document whether functions accept nil for optional parameters.
- Embed types deliberately, not for faux inheritance. Promoted methods should read as natural capabilities of the outer type.
- Prefer named struct fields in literals. Positional literals break when fields are reordered or added.
D - Packages, layout, and tooling
- Keep
mainthin; put logic in importable packages. One module can hostcmd/entrypoints and library code side by side. - Use
internal/for code that must not leak across modules. Let the compiler enforce boundaries instead of comments alone. - Run
go fmt ./...before every commit. Consistent formatting removes style debates from review. - Run
go test ./...andgo vet ./...in CI. Fundamentals mistakes should fail fast in automation. - Pin module versions in
go.modand commitgo.sum. Reproducible builds start with a clean module graph.
FAQs
Should I always use pointers for structs?
No - small structs are cheaper by value.
Use pointers for mutation, optional fields, or proven copy cost.
Is it wrong to use package-level variables?
Constants and read-only vars are fine.
Mutable package state needs synchronization and hurts testability - avoid unless wrapping sync primitives by design.
When should I use arrays instead of slices?
Rarely - fixed wire sizes, cryptographic blocks, or embedded constraints.
Default to slices for application code.
How strict should embedding be?
Embed when the outer type genuinely exposes the embedded behavior (Server logs via embedded Logger).
Use named fields when the relationship is has-a, not behaves-as.
Why assign append results?
append may reallocate and return a different slice header.
Ignoring the return leaves you pointing at stale capacity or data.
Should I name every return value?
Named returns help documentation in small functions; avoid when they obscure clarity.
Fundamentals code should favor explicit return x, err.
How do I avoid loop variable bugs in goroutines?
Pass loop variables as function parameters.
On Go 1.22+, per-iteration variables reduce the risk but explicit parameters stay clearest.
What belongs in cmd/ vs package root?
cmd/<app>/main.go holds executables; library code lives in importable packages at module root or pkg/ by team convention.
Is nil slice OK in JSON APIs?
nil slices often encode as null; empty slices as [].
Pick intentionally for API contracts and document the choice.
When should I use internal/?
Whenever code must not be imported by other modules - helpers tied to one product, not a public SDK.
Do I need constructors for every struct?
Only when validation or invariants are required.
Prefer useful zero values for simple configuration structs.
How do best practices relate to golangci-lint?
Enable linters like govet, staticcheck, and errcheck to enforce many of these habits automatically in CI.
Related
- Go Fundamentals Basics - Runnable intro covering these habits in code
- How Go Programs Are Structured - Package layout and toolchain mental model
- Variables, Constants & Scope - Declaration and scope rules in depth
- Zero Values and Initialization - Safe defaults and
makepatterns - Pointers & the Address-of Operator - When pointers earn their complexity
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).