Essential Go Libraries Best Practices
A condensed summary of the 25 most important essential-library practices drawn from every page in this section.
-
Stdlib first: Reach for
log/slog,net/http, anddatabase/sqlbefore adding modules; document the capability gap when you import. -
One library per concern: Pick one logger, one router, and one config loader per service; duplicate tools inflate onboarding and CVE surface.
-
Review go.mod diffs: Treat new
requirelines like production code in PR review; rungo mod tidybefore merge. -
Run govulncheck in CI: Scan the full module graph after dependency changes; block merges on critical paths your policy defines.
-
Check LICENSE files: Confirm BSD/MIT/Apache compatibility before importing; legal review for LGPL or unusual terms.
-
Inspect maintenance signals: Favor libraries with recent releases, responsive issues, and clear ownership over stale GitHub stars.
-
Measure transitive depth: Run
go list -m allwhen evaluating imports; shallow graphs age better than surprise forks. -
Keep libraries at edges: Domain packages accept interfaces; zap, chi, and viper stop at handlers,
main, andinternal/config. -
Inject loggers: Pass
*slog.Loggeror*zap.Loggerfrommain; avoid package-level defaults that tests cannot override. -
Stabilize log field names: Use consistent keys (
trace_id,err,user_id) across services so migrations between zap, zerolog, and slog do not break queries. -
Default new services to slog: Adopt zap or zerolog only when benchmarks on real handlers prove allocation bottlenecks.
-
Load config once in main: Unmarshal viper or envconfig into typed structs; never scatter
viper.GetStringthrough business packages. -
Validate required secrets at startup: Fail fast when DSNs or API keys are empty; do not discover missing config on first traffic.
-
Document env var tables: Mirror struct tags in README and Helm comments so operators know required keys without reading code.
-
Use testify in tests only: Keep assertions in
_test.go; library consumers should not inherit testify transitively from your API. -
Prefer small interfaces for mocks: Define one- or two-method interfaces at the consumer; generate mocks with mockery only when call contracts matter.
-
Regenerate Wire after provider changes: Run
wire ./...in CI and fail on stalewire_gen.godiffs when constructors move. -
Split Wire injector sets: Use separate provider sets per binary (
API,worker) instead of one god-graph initializer. -
Validate at the HTTP edge: Run
validatoron DTOs after JSON decode; keep business invariants in services, not tags alone. -
Run migrations in deploy jobs: Apply golang-migrate
upbefore new code serves traffic; never migrate inside request handlers. -
Keep sqlc schema in sync: Point sqlc at the same migration folder CI uses; regenerate Go when queries or columns change.
-
Guard Prometheus cardinality: Label metrics with route patterns, not raw URLs with IDs; cap label dimensions in ADRs.
-
Shutdown OTel exporters on SIGTERM: Call
TracerProvider.Shutdownduring graceful drain so spans flush before exit. -
Expose /metrics safely: Scrape on an admin port or behind network policy; do not publish operational metrics on the public internet without auth.
-
Record library choices in ADRs: Document why chi over Gin, slog over zap, or sqlc over ORM so future teams understand constraints and migration paths.
FAQs
How often should we revisit library choices?
After major Go releases and when security advisories affect your graph - at least quarterly for platform-owned stacks.
What is the highest-leverage habit in this section?
Stdlib-first plus minimal deps - it keeps builds fast, CVE blast radius small, and onboarding predictable.
Should platform teams ban libraries outright?
Prefer allowlists with exceptions via ADR rather than ad hoc bans per service without governance.
How do best practices differ for published modules?
Libraries face stricter transitive dependency limits; consumers inherit your entire graph.
When is copying better than importing?
When the logic is under roughly thirty lines, security-reviewed, and unlikely to need upstream fixes - document the fork decision.
Do CLIs follow the same rules as services?
Yes for logging and config hygiene; CLIs may skip HTTP routers and metrics unless they expose servers.
How does this relate to go-rules module cheatsheet?
Module semver rules complement these library practices - use both in PR templates.
What is the most common logging mistake?
Inconsistent field keys across services, which breaks dashboards during library migrations.
Should we vendor essential libraries?
Only when policy requires; vendoring does not remove the need to vet versions and run govulncheck.
Where do observability section practices overlap?
Slog, Prometheus RED, and OTel tracing articles drill deeper; this section names the library picks at the edge.
Related
- The Go Ecosystem: Small, Focused Libraries - cultural context for dependency choices
- Essential Libraries Basics - starter module examples
- Module & Dependency Rules - go.mod hygiene cheatsheet
- The Go Standard Library: Batteries Included - stdlib-first baseline
- govulncheck for Dependency Vulnerabilities - supply-chain scanning
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).