Skip to content
compiler.dev

Go GitHub Actions Workflow Template

Last updated

This is a working .github/workflows/ci.yml for a Go module. It reads the Go version from go.mod, caches modules and build output, runs go vet, golangci-lint and the tests with the race detector.

The workflow

name: CI

on:
  push:
    branches: [main]
  pull_request:

permissions:
  contents: read

concurrency:
  group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
  lint:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version-file: go.mod
      - run: go vet ./...
      - uses: golangci/golangci-lint-action@v8
        with:
          version: latest

  test:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version-file: go.mod
          cache-dependency-path: go.sum
      - run: go build ./...
      - run: go test -race -count=1 -coverprofile=coverage.out ./...

What the non-obvious parts do

  • go-version-file: go.mod uses the version your module declares, so CI follows the go directive without a second place to update.
  • Caching is automatic. setup-go v4 and later caches the module download cache and the build cache, keyed on go.sum. This is why there is no separate actions/cache step. cache-dependency-path is only needed when go.sum is not at the repository root, for example in a monorepo.
  • Two jobs. Lint and test run in parallel on separate machines, so you get the faster of the two as feedback and the slower as total time.
  • golangci-lint-action manages its own cache and annotates the diff. Pin a lint version, not latest, if you need repeatable results. Check the action's README for the current major version before copying.
  • go test -race enables the race detector. It slows tests and uses more memory, but catches real concurrency bugs. Remove it if you must, but try it first.
  • -count=1 disables Go's test result caching. Results are cached per machine only, so on fresh CI runners it makes no difference, but it keeps runs identical to local -count=1 runs.

Variants

Version matrix

Add matrix.go: ["1.23", "1.24"] and use go-version: ${{ matrix.go }} instead of go-version-file. Only needed for libraries; applications should test the version they ship. See matrix builds.

Database tests

Add a services: block. The Django template shows a Postgres service with health checks, and the same block works for Go.

Monorepo

Use working-directory: and a paths: filter, and point cache-dependency-path at each module's go.sum.

Build and release

Add a job that runs go build -trimpath -ldflags="-s -w" and upload the binary as an artifact, or use GoReleaser on tags.

macOS or Windows

Add them to a matrix if you ship binaries for those systems.

Run it on faster machines

Go compiles and tests in parallel, so more cores help directly. Change one line on each job:

    runs-on: [self-hosted, Linux, compiler-ci]

The job runs on a fresh 8 vCPU machine from compiler.dev, with a fallback to your original runner. compiler.dev's benchmark of a React and Go app with go test -race went from 6m13s to 2m42s; see benchmarks. More in speed up Go on GitHub Actions and on pricing.

FAQ

Do I need actions/cache for Go modules?

Not with setup-go v4 or later. It caches modules and build output by default.

Why is the first run slow and later runs fast?

The first run populates the build cache. Later runs reuse compiled packages.

Should I run the race detector on every push?

If it fits your time budget, yes. If not, run it on main and nightly.

How do I stop lint from failing on old code?

Set only-new-issues: true in the lint action.