Go Toolchain Best Practices
Reproducible builds, CI integration, and toolchain pinning.
These rules keep go command workflows consistent from laptops through CI to release artifacts.
How to Use This List
- Apply during PR review when dependency, build, or test commands change.
- Mirror the same
GOPRIVATE,GOPROXY, andGOTOOLCHAINvalues in CI and developer docs. - Run
go mod tidy,go test ./..., and linters on every dependency bump before merge. - Revisit after Go minor upgrades; modernizers and vet analyzers expand each release.
A - Toolchain and module hygiene
- Pin the Go version in
go.modand documentGOTOOLCHAINpolicy. Teammates and CI should resolve the same compiler; useautoonly when you accept automatic downloads. - Run
go mod tidyafter any import or dependency change. Keepsrequirelines minimal andgo.sumaccurate. - Commit
go.sumwhenevergo.modchanges. Checksums are part of the reproducible build contract. - Set
GOPRIVATEfor every internal module prefix. Prevents leaking private paths to the public proxy and sum DB. - Put an internal proxy (Athens/Artifactory) ahead of
proxy.golang.org. Speeds CI and survives upstream outages.
B - Build and release commands
- Ship
go buildartifacts, notgo run, in production.runrecompiles every invocation and hides the binary path. - Use
-trimpathand-ldflags="-s -w"for release binaries. Shrinks size and strips host paths from stacks. - Name outputs explicitly with
-o bin/<name>. Avoids collisions when building multiple commands in one pipeline step. - Cross-compile with
CGO_ENABLED=0unless you truly need cgo. Pure Go matrices are simpler on one Linux builder. - Record
go versionandgo envin release logs. Speeds debugging when CI and laptop builds diverge.
C - Test, vet, and lint gates
- Run
go test ./...(or./...with-racein CI) on every PR. The default gate before merge. - Run
go vet ./...before optional linters. Zero-config baseline from the stdlib. - Run
golangci-lint(or staticcheck) with a committed config. Team-wide rules beyond vet. - Use
go test -count=1while debugging flakes; fix root cause before merge. Do not rely on disabled test cache in production CI forever. - Run
go fix -diffafter Go upgrades, then apply with review. Modernizers catch idioms training data may miss.
D - Cache, vendor, and hermetic CI
- Cache
GOMODCACHEandGOCACHEin CI.go mod downloadwarm steps pay off on every job. - Use
-mod=readonlyin CI when not vendoring. Fails builds that mutatego.modsilently. - Re-vendor in the same commit as dependency bumps when using
vendor/. Drift breaks-mod=vendorpipelines. - Avoid
go clean -modcachein routine CI. Only clear caches when corruption is proven. - Verify private module auth in CI with a dry-run
go mod download. Catch token expiry before compile steps.
E - Codegen and tags
- Pin generator versions in
//go:generatedirectives.go run tool@versionbeats undocumented local binaries. - Fail CI on
go generatedrift (git diff --exit-code). Generated output stays in sync with sources. - Document required
-tagsfor production builds. Defaultgo testmay skip tagged files CI never exercises. - Matrix-test critical tag combinations. At least default and production tag sets.
- Keep build tags for compile-time backends, not ops feature toggles. Runtime config avoids rebuild churn.
FAQs
What is the minimum CI pipeline for Go?
go mod download, go test ./..., go vet ./..., and optionally golangci-lint run.
Add -race when runners have enough CPU and RAM.
Should I vendor?
Vendor when policy requires in-repo third-party source or builds must not reach module proxies.
Otherwise prefer proxy plus cached GOMODCACHE.
How do I pin patch versions?
Set go 1.26.x in go.mod and GOTOOLCHAIN=go1.26.x in CI.
Let patch auto-download only if the team agrees.
When should modernizers run?
During upgrade branches, locally with go fix -diff, then commit reviewed changes.
Not on every unrelated PR.
How do private modules and Athens interact?
Athens caches downloads inside your network.
GOPRIVATE still marks which paths must never hit the public index.
Should integration tests use tags?
Yes.
Keep slow integration suites behind integration tags and run them nightly or on main.
What env vars belong in repo docs?
GOPRIVATE, GOPROXY, GONOSUMDB, GOTOOLCHAIN, and any auth setup for Git hosts.
Not secrets themselves - reference the secret store.
How do I speed up go test in CI?
Cache GOCACHE, split -race to a dedicated job, and use -shuffle=on only when hunting order bugs.
Avoid -a unless debugging cache issues.
Is gofmt still required?
Yes.
Enforce with gofmt -l or golangci-lint gofmt/goimports linters in CI.
What should release artifacts include?
Binary per target, go version string embedded via -ldflags, and SBOM input from go list -m -json all.
Related
- The go Command: Build, Test, and Module Lifecycle - conceptual overview
- Go Toolchain Basics - command examples
- go build, run, install & clean - release flags
- go mod tidy, vendor, download & graph - dependency workflows
- Cross-Compilation & Private Module Proxies - env configuration
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).