go / checks
I run five checks before committing Go code:
goimports -local "$(go list -m)" -w .
go vet ./...
go test ./...
deadcode -test ./...
gopls check -severity=hint ./*.go
Order
The checks run fast to slow:
- goimports: Formats code and fixes imports.
- go vet: Static analysis.
- go test: Runs tests.
- deadcode: Finds unreachable functions with whole-program analysis.
- gopls: Reports the diagnostics the editor shows. Loads the whole workspace.
goimports
goimports
formats code like gofmt and also adds/removes imports.
go install golang.org/x/tools/cmd/goimports@latest
I install it from my laptop script.
The -local flag groups imports into three sections:
standard library, third-party, and local module.
import (
"fmt"
"net/http"
"github.com/someone/pkg"
"mymodule/internal/foo"
)
go vet
go vet reports likely mistakes such as printf format errors. It ships with Go.
go test
go test runs tests.
./... matches all packages in the module. See go / test
for the flags, assertions, and fixtures.
deadcode
deadcode finds functions that are never called.
go install golang.org/x/tools/cmd/deadcode@latest
I install it from my laptop script.
It analyzes the whole program from main, so it works only on
executables.
The -test flag includes test binaries in the analysis:
deadcode -test ./...
I remove an unreachable function or write a test that reaches it.
The -test flag helps most in a project with an entry point deadcode
cannot analyze, such as WASM.
gopls
gopls, the Go language
server, reports diagnostics beyond go vet: modernizers that flag
older idioms, and hints like an unchecked bufio.Scanner.Err. I run
it as a check so these show up before review.
go install golang.org/x/tools/gopls@latest
With -severity=hint, a hint fails the check. gopls check takes
file paths, so I pass ./*.go.
On a large repo, checking every file is slow. There I scope the check
to the files a branch changed against main:
git diff --name-only --diff-filter=d origin/main...HEAD -- '*.go' |
xargs gopls check -severity=hint
A Go or gopls upgrade can add hints to untouched files. A full scan catches those:
git ls-files -z '*.go' | xargs -0 gopls check -severity=hint
Pinned tools
A check is reproducible only when every machine runs the same tool
version. Go 1.24 added the tool directive for this. My SQL formatter
uses it:
go get -tool github.com/croaky/pgfmt/cmd/pgfmt
go tool pgfmt -c <pkg>/queries/*.sql
The version lives in go.mod, so go tool builds the same version on
my laptop and in CI, with nothing on $PATH. An upgrade is a commit
that also reformats what the new version formats differently.
The five checks above are still go install ...@latest from my
laptop script, so the version depends on the day I last
ran it. Two CI boxes provisioned a month apart run different linters.
gopls varies most, since hints change between versions.
I have not moved them to tool directives because of their
dependencies: goimports and deadcode add five indirect requires,
and gopls adds 23. Those join the main module's build list.
How they run
The commands live in two places:
AGENTS.mdlists the checks with a rule for each: run X if Y files touched. See ai/agents.- cibot runs them in CI, one check per
Checkfileline.
Project-specific checks live alongside these. One repo's Checkfile
also runs nullscan. A check can also read the test
run's output rather than run its own: my template
render coverage check reads the
-v output of the go test job, so the suite runs once.
govulncheck
govulncheck checks the Go Vulnerability Database and reports only vulnerabilities the code reaches. Dependabot cannot filter that way. See Filippo Valsorda's Turn Dependabot Off.
I run it periodically:
go run golang.org/x/vuln/cmd/govulncheck@latest ./...
Latest dependencies
I also test against the newest dependency versions:
go get -u -t ./...
go mod tidy
go test ./...
git checkout go.mod go.sum # restore pinned versions