gofmt & goimports Enforcement
gofmt and goimports are the formatting layer every Go repo should automate.
Teams enforce them in editors, pre-commit hooks, and CI so reviews focus on behavior instead of whitespace.
Recipe
Quick-reference recipe card - copy-paste ready.
# Format all packages in place
gofmt -w .
goimports -local example.com/mycorp -w .
# CI: fail if any file would change
test -z "$(gofmt -l .)"
test -z "$(goimports -local example.com/mycorp -l .)"
# Optional simplifications
gofmt -s -w .When to reach for this:
- Bootstrapping CONTRIBUTING.md for a new Go service.
- Fixing "formatting only" PR noise in a busy monorepo.
- Onboarding engineers from languages with configurable formatters.
- Auditing whether generated protobuf still belongs in format checks.
Working Example
Repository layout:
example.com/widget/
go.mod # module example.com/mycorp/widget
cmd/api/main.go
internal/store/store.gocmd/api/main.go before enforcement:
package main
import (
"fmt"
"example.com/mycorp/widget/internal/store"
"net/http"
)
func main() {
store := store.New()
http.HandleFunc("/ok", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, store.Ping())
})
http.ListenAndServe(":8080", nil)
}Enforce:
goimports -local example.com/mycorp -w ./cmd ./internal
gofmt -s -w ./cmd ./internalAfter (import grouping shows -local effect):
package main
import (
"fmt"
"net/http"
"example.com/mycorp/widget/internal/store"
)
func main() {
store := store.New()
http.HandleFunc("/ok", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, store.Ping())
})
http.ListenAndServe(":8080", nil)
}GitHub Actions excerpt:
- name: Format check
run: |
test -z "$(gofmt -l .)"
test -z "$(goimports -local example.com/mycorp -l .)"What this demonstrates:
- goimports fixes indentation and import order in one pass.
-localkeeps internal modules in a separate import group.- CI fails on drift without rewriting contributor machines.
Deep Dive
How It Works
- gofmt parses Go source and prints canonical spacing, indentation, and line breaks.
- goimports calls gofmt after adjusting import declarations.
- Both are deterministic: no
.editorconfigoverrides for tab width. - gopls can apply the same transformations on save when
formatting.gofumptor goimports settings are enabled.
Import Group Order
| Group | Example paths | Separator |
|---|---|---|
| Standard library | fmt, net/http | blank line below |
| Third-party | github.com/go-chi/chi/v5 | blank line below |
Local (-local prefix) | example.com/mycorp/widget/internal/store | end of block |
Enforcement Modes
| Mode | Command | Best for |
|---|---|---|
| Check only | gofmt -l . | CI gates |
| Fix locally | gofmt -w . | pre-commit |
| Fix in CI bot | auto-commit job | high-churn OSS |
| Editor on save | gopls formatting | daily editing |
Go Notes
# Verify tool versions match CI
gofmt -V # bundled with go version
go version -m $(which goimports) | head -1Pin golang.org/x/tools pseudo-version in tools.go for reproducible go install of goimports across laptops and agents.
Flags Worth Knowing
| Flag | Tool | Effect |
|---|---|---|
-w | both | Write formatted file |
-l | both | List files that would change |
-s | gofmt | Apply simplifications |
-local | goimports | Prefixes for local import group |
Gotchas
- Checking gofmt but editing with goimports - Contributors pass locally but CI fails on import grouping. Fix: Standardize on goimports in hooks and CI.
- Forgetting
-localin CI - Internal imports mingle with third-party paths. Fix: Pass the same-localprefixes in docs, hooks, and workflows. - Formatting generated protobuf - Huge diffs on regen fights code owners. Fix: Exclude
*.pb.gofromgofmt -lpaths or run format as part of codegen. - Mixed Go versions in monorepo - gofmt output rarely differs, but goimports behavior can shift with x/tools version. Fix: Pin tools via
tools.goandgo installin CI. - Reviewing unformatted patches - Diffs hide logic in whitespace noise. Fix: Require format before opening PR (
make fmttarget). - Using
go fmtas a check -go fmtalways rewrites files; it is not a list-only CI pattern. Fix: Usegofmt -lor dedicated lint job.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| gofumpt | Stricter spacing rules than gofmt | OSS libs where contributors expect stock gofmt |
golangci-lint gofmt linter | Already running golangci-lint for everything | Tiny repos wanting zero YAML |
| Manual review only | Never for production Go | - |
cargo fmt-style config files | - | Go ecosystem does not support this model |
FAQs
Should we use gofmt or goimports in CI?
Pick one primary check.
Most teams run goimports (which includes gofmt) locally and verify with goimports -l in CI.
What does gofmt -s change?
Simplifications like s[a:len(s)] to s[a:].
Optional but common in hooks; document if your CI requires -s.
How do I format only changed files in a hook?
Use git diff --name-only --cached '*.go' | xargs goimports -w to limit scope and keep hooks fast.
Does gopls use gofmt or goimports?
Configurable via editor settings.
Align gopls with CI so on-save matches pipeline expectations.
Should vendor/ be formatted?
Usually no - vendor is third-party source copied verbatim.
Exclude vendor paths from format checks.
Can I enforce format on non-Go files?
Out of scope for gofmt.
Use Prettier or language-specific formatters for YAML, Markdown, and protobuf .proto sources.
What about build tags in formatted files?
gofmt preserves build tags at the top of the file.
Run formatters on tagged files with the same tags CI uses when analyzing.
How do monorepos pass multiple -local prefixes?
Repeat the flag: goimports -local example.com/a -local example.com/b -w ..
Is failing CI on format hostile to drive-by contributors?
Provide make fmt and optional bot auto-fix.
Consistency lowers long-term friction more than one-time format commits.
Do templates in go:generate need formatting?
Generated output should be formatted by the generator or a post-step.
Do not hand-format generated files that regen overwrites.
Related
- Go Code Quality: Opinionated by Design - why format is non-negotiable
- Code Quality Basics - runnable format and vet intro
- Pre-commit Hooks & Review Checklists - hook wiring
- golangci-lint Configuration - gofmt linter inside golangci-lint
- Coding Standards & Doc Conventions for Teams - broader team standards
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).