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:

  1. goimports: Formats code and fixes imports. Runs first so other tools see properly formatted code.
  2. go vet: Static analysis. Catches bugs before spending time on tests.
  3. go test: Runs tests. No point running if vet already found issues.
  4. 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:

  1. Remove it. The function is genuinely unused.
  2. 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:

Security: govulncheck

Replace Dependabot security alerts with a scheduled scan that only reports reachable vulnerabilities. Create .github/workflows/govulncheck.yml:

name: govulncheck
on:
  schedule: # daily at 10:22 UTC
    - cron: '22 10 * * *'
  workflow_dispatch:
permissions:
  contents: read
jobs:
  govulncheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false
      - uses: actions/setup-go@v5
        with:
          go-version-file: go.mod
      - run: go run golang.org/x/vuln/cmd/govulncheck@latest ./...

govulncheck uses the Go Vulnerability Database and static analysis to filter out vulnerabilities that don't affect your code. Dependabot can't do this, so it opens noisy PRs for vulnerabilities in packages you don't even call. See Filippo Valsorda's Turn Dependabot Off.

CI: test against latest dependencies (without updating)

Run CI daily against the newest dependency versions to catch breakage early, without opening PRs. Create .github/workflows/latest-deps.yml:

name: latest-deps
on:
  schedule:
    - cron: '15 10 * * *' # daily at 10:15 UTC
  workflow_dispatch:
jobs:
  test-latest:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version-file: go.mod
      - run: |
          go get -u -t ./...
          go mod tidy
          go test ./...

← All articles