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:

  1. goimports: Formats code and fixes imports.
  2. go vet: Static analysis.
  3. go test: Runs tests.
  4. deadcode: Finds unreachable functions with whole-program analysis.
  5. 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:

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

← All articles