go / checks
I run four checks before committing Go code:
goimports -local "$(go list -m)" -w .
go vet ./...
go test ./...
deadcode -test ./...
Order
The checks run fast-to-slow to fail fast:
- goimports: Formats code and fixes imports. Runs first so other tools see properly formatted code.
- go vet: Static analysis. Catches bugs before spending time on tests.
- go test: Runs tests. No point running if vet already found issues.
- deadcode: Finds unreachable functions. Slowest (whole-program analysis), informational.
goimports
goimports
formats code like gofmt and also adds/removes imports.
go install golang.org/x/tools/cmd/goimports@latest
I install this via 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: printf format errors, unreachable code, suspicious constructs.
It's built into Go and runs fast.
go test
go test runs tests.
The ./... pattern matches all packages in the module.
deadcode
deadcode finds functions that are never called.
go install golang.org/x/tools/cmd/deadcode@latest
I install this via my laptop script.
It uses whole-program analysis starting from main,
so it only works on executables, not libraries.
The -test flag includes test binaries in the analysis:
deadcode -test ./...
This creates a virtuous cycle for codebase quality. When deadcode reports an unreachable function, you have two options:
- Remove it. The function is genuinely unused.
- Add a test. The function is used but not covered by tests.
Either outcome improves the codebase: less dead code or better test coverage.
The -test flag is especially useful for projects with multiple entry points
(WASM, CLI tools, etc.) where some functions are only reachable
from entry points that deadcode can't analyze natively.
GitHub Actions
To run these checks in CI, create .github/workflows/ci.yml:
name: ci
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Install tools
run: |
go install golang.org/x/tools/cmd/goimports@latest
go install golang.org/x/tools/cmd/deadcode@latest
- name: Format Go
run: test -z "$(goimports -local "$(go list -m)" -l .)"
- name: Static checks
run: go vet ./...
- name: Run tests
run: go test -v ./...
- name: Find unreachable functions
run: deadcode -test ./...
Notes:
go-version-file: go.modreads the Go version from go.mod, avoiding version drift between local and CI.goimports -llists unformatted files (without writing).test -zfails if the list is non-empty.go test -vuses verbose output to help debug CI failures.
Dependabot
To automate dependency updates, create .github/dependabot.yml:
version: 2
updates:
- package-ecosystem: gomod
directory: "/"
schedule:
interval: monthly
open-pull-requests-limit: 5
Dependabot will open PRs to update Go modules monthly. The CI workflow runs on PRs, so updates are tested before merging.