Post

Go Build: Go Toolchain, Modules and GoReleaser

How the Go toolchain builds, tests and secures Go services, which files it needs, how GoReleaser and ko package them, and a complete GitHub Actions pipeline.

Go Build: Go Toolchain, Modules and GoReleaser

Go ships its build system inside the language toolchain: go build, go test, go vet and Go modules cover what other languages need three or four tools for. DevOps work in Go is mostly about reproducibility, static binaries, vulnerability checks and packaging releases for many platforms.

This post is part of Build Ecosystems by Programming Language.

At a glance

CategoryCommon tools
Build toolsgo build, go install; orchestration with Make / Task / Mage; releases with GoReleaser; images with ko; Bazel (rules_go) for monorepos
Package registryGo modules via proxy.golang.org, checksum database sum.golang.org, private GOPROXY (Artifactory, Athens)
Build filesgo.mod, go.sum, go.work, Makefile, .goreleaser.yaml, .golangci.yml, .ko.yaml
Lint and formatgofmt, go vet, staticcheck, golangci-lint
SAST (open source)gosec, Semgrep, CodeQL, SonarQube Community Build
SAST (commercial)Checkmarx One, Veracode, Snyk Code, SonarQube Server
SCAgovulncheck (call-graph aware), OSV-Scanner, Trivy, Grype, Snyk, Mend
Frameworksnet/http, Gin, Echo, chi, Fiber, gRPC-Go, Cobra (CLIs)
Test and coveragego test, testify, gomock, go test -cover, go tool cover
Build outputStatic binary per OS/arch, archives and checksums (GoReleaser), OCI images (ko)
CI cache~/go/pkg/mod (module cache), ~/.cache/go-build (build cache)

How the build works

In CI, each stage of the generic build pipeline maps to a Go toolchain command, plugin or scanner. Press play, or step through it:

  1. Source + go.mod: A commit triggers CI. go.mod lists the module requirements and go.sum records their checksums.
  2. Download and verify modules: go mod download fetches modules through GOPROXY (proxy.golang.org or a private proxy), and go mod verify checks them against go.sum.
  3. Lint and format: gofmt -l must print nothing, then go vet, staticcheck and golangci-lint catch suspicious code.
  4. Compile: CGO_ENABLED=0 go build -trimpath produces a static binary, with the version stamped in through -ldflags.
  5. Test and coverage: go test -race -coverprofile=cover.out ./... runs the tests with the race detector; go tool cover summarises coverage.
  6. Security scans in parallel: gosec checks our code, govulncheck reports only vulnerabilities in code paths we actually call, and Gitleaks scans for secrets.
  7. Quality gate: Formatting, vet, lint, tests and both scanners are required checks; any failure stops the release.
  8. Fail the build (fail): A failed check stops the pipeline at the gate, so nothing is packaged, signed or published.
  9. Cross-compile and package (pass): GoReleaser cross-compiles for each OS and architecture and builds archives and checksums from .goreleaser.yaml.
  10. SBOM and signing (pass): GoReleaser produces SBOMs and signatures from the same config; cyclonedx-gomod is the standalone SBOM tool.
  11. Store the release (pass): Binaries and archives are kept as release assets or in an artifact repository; the CI job stores them with actions/upload-artifact.
  12. Container image (pass): ko builds a minimal container image for the Go binary without a Dockerfile, and the registry scans it again.
  • Minimal version selection picks the lowest version that satisfies every requirement, so builds are reproducible without a separate lockfile; go.sum stores the checksums.
  • The build cache makes repeated builds and tests fast; cache it in CI.
  • CGO_ENABLED=0 produces a fully static binary that runs in scratch or distroless images.
  • Cross-compiling is built in: GOOS=linux GOARCH=arm64 go build ./cmd/app.

Build files

FilePurposeCommit?
go.modModule path, Go version, toolchain line, requirementsYes
go.sumChecksums of every module version usedYes
go.workLocal multi-module workspaceUsually no (developer convenience)
vendor/Vendored dependencies (go mod vendor)Optional, for air-gapped builds
Makefile / Taskfile.ymlStandard targets: build, lint, test, releaseYes
.goreleaser.yamlCross-platform builds, archives, checksums, SBOMs, imagesYes
.golangci.ymlEnabled linters and settingsYes
.ko.yamlBase image and build settings for koYes

Commands by category

1
2
3
4
5
6
7
8
9
go mod download && go mod verify                 # fetch and verify modules
CGO_ENABLED=0 go build -trimpath -ldflags="-s -w -X main.version=1.0.0" -o bin/app ./cmd/app   # build
go vet ./... && golangci-lint run                # lint
gosec ./...                                      # SAST
govulncheck ./...                                # SCA (only vulnerable code you actually call)
go test -race -coverprofile=cover.out ./...      # tests + coverage
go tool cover -func=cover.out                    # coverage summary
cyclonedx-gomod app -json -output sbom.json ./cmd/app   # SBOM
goreleaser release --snapshot --clean            # multi-platform release locally

Minimal project

1
2
3
4
5
6
// go.mod
module github.com/example/demo

go 1.24

require github.com/go-chi/chi/v5 v5.2.0
1
2
3
4
5
6
7
8
9
10
11
12
# Makefile (recipes must be indented with tabs)
VERSION ?= $(shell git describe --tags --always)

.PHONY: build lint test vuln
build:
	CGO_ENABLED=0 go build -trimpath -ldflags="-s -w -X main.version=$(VERSION)" -o bin/app ./cmd/app
lint:
	golangci-lint run
test:
	go test -race -cover ./...
vuln:
	govulncheck ./...

Frameworks

FrameworkStart a project
net/http (standard library)go mod init example.com/api and use http.HandleFunc
Gingo get github.com/gin-gonic/gin
Echogo get github.com/labstack/echo/v4
chigo get github.com/go-chi/chi/v5
Cobra (CLI)go install github.com/spf13/cobra-cli@latest && cobra-cli init

CI with GitHub Actions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
name: go-ci
on: [push, pull_request]
permissions:
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version-file: go.mod              # caches module + build cache automatically

      - run: go mod verify
      - run: test -z "$(gofmt -l .)"           # fail on unformatted files
      - run: go vet ./...
      - run: go run honnef.co/go/tools/cmd/staticcheck@latest ./...
      - run: go run github.com/securego/gosec/v2/cmd/gosec@latest ./...
      - run: go run golang.org/x/vuln/cmd/govulncheck@latest ./...
      - run: go test -race -coverprofile=cover.out ./...
      - run: CGO_ENABLED=0 go build -trimpath -ldflags="-s -w -X main.version=${{ github.sha }}" -o bin/app ./cmd/app

      - uses: actions/upload-artifact@v4
        with:
          name: app
          path: bin/app

Pin tool versions (staticcheck@2025.1, gosec@v2.22.0) instead of @latest once the pipeline is stable, so a new linter release cannot break unrelated builds.

Best practices

  • Commit go.sum and run go mod verify; use go mod tidy -diff in CI to catch an untidy go.mod.
  • Set GOFLAGS=-mod=readonly so builds never silently modify go.mod.
  • Use GOPRIVATE and a private GOPROXY for internal modules instead of committing tokens.
  • Build static, stripped binaries (CGO_ENABLED=0, -trimpath, -ldflags="-s -w") and inject the version with -X.
  • Prefer govulncheck as the gate: it reports only vulnerabilities in code paths you call, so it has far fewer false positives than manifest-only scanners.
  • Release with GoReleaser to get archives, checksums, SBOMs and signed images from one config.
This post is licensed under CC BY 4.0 by the author.