Go Tooling

Set Up and Configure golangci-lint for a Go Project

Set up golangci-lint, migrate to a v2 YAML config, tune enabled linters, verify the configuration, and keep CI runs reproducible.

By Niko Minadze5 min read

Editorial illustration for Set Up and Configure golangci-lint for a Go Project

The practical setup is to choose one golangci-lint release, use a v2 .golangci.yml with an explicit linter policy, and run the same command from the module root locally and in CI. Pinning matters: a newly added linter or an upgraded upstream linter can otherwise turn previously passing builds red without a source-code change.

There is one version boundary to keep straight. The supplied v1.64.8 installation examples come from the legacy v1 installation documentation, while version: "2" and linters.default belong to the v2 configuration format. Treat those as separate setup tracks rather than assuming a v1.64.8 binary accepts a v2 configuration.

Install a known release

The golangci-lint documentation recommends installing a specific release for reproducible CI. It specifically warns that --enable-all can begin failing builds when a new linter is added; even without that option, an upgraded upstream linter can change results.

For the documented legacy v1.64.8 release, the installation script can place the binary in $(go env GOPATH)/bin:

curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.64.8

Alpine Linux does not include curl by default, so the legacy documentation gives this wget variant:

wget -O- -nv https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s v1.64.8

A package manager or go install is convenient for a developer workstation:

brew install golangci-lint
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest

These two commands do not express a specific release in the command itself. That makes them less suitable as written for a pipeline that requires a deliberately pinned version. For a v2 migration, select a specific v2 release and use the matching official installation instructions rather than substituting the legacy v1.64.8 examples.

If the project intentionally remains on v1.64.8, the documented Docker invocation is:

docker run --rm -v $(pwd):/app -w /app golangci/golangci-lint:v1.64.8 golangci-lint run -v

To preserve the golangci-lint cache between container runs, mount a cache directory as well:

docker run --rm \
  -v $(pwd):/app \
  -v ~/.cache/golangci-lint/v1.64.8:/root/.cache \
  -w /app \
  golangci/golangci-lint:v1.64.8 \
  golangci-lint run -v

After installation, inspect the binary selected by your PATH:

golangci-lint --version

Compare that output with the release your project intends to use before debugging configuration differences.

Run once before adding configuration

From the Go module root, run:

golangci-lint run

A plain run uses the default linter set and examines the current package tree. golangci-lint runs multiple linters in one pass and presents their findings as unified output, rather than requiring a separate command for every linter.

Running before creating .golangci.yml gives you a useful baseline. Read the reported findings, then decide which additional checks belong in the project policy. Avoid starting with every available linter merely because the switch exists; that makes future linter additions capable of changing the build unexpectedly.

Write a v2 .golangci.yml

The current v2 format begins with version: "2". Here is a complete starter configuration:

version: "2"

linters:
  default: standard
  enable:
    - bodyclose
    - contextcheck
    - errname
    - errorlint
    - exhaustive
    - gocognit
    - gosec
    - nestif
    - noctx
    - prealloc
    - unconvert
    - unparam
  settings:
    gocognit:
      min-complexity: 20
    errcheck:
      check-type-assertions: true

linters.default accepts all, standard, none, or fast. Using standard provides a baseline while the enable list makes the project’s additional choices visible in code review.

Several structural changes matter when migrating a v1 configuration:

  • The old enable-all and disable-all options are replaced by linters.default.
  • Per-linter settings now belong under the linters section.
  • File-path options are relative to the configuration file by default, not to the directory where the binary was launched.

That final change can alter which files a path-based rule addresses, especially when CI launches the command from a different working directory. Review such paths during migration instead of mechanically moving the YAML keys.

Tune and verify the configuration

The example sets gocognit’s min-complexity to 20 and enables errcheck’s check-type-assertions setting. Keep settings in the same file as the enabled-linter policy so local and CI runs can share it.

For a quicker feedback mode, v2 provides --fast-only:

golangci-lint run --fast-only

This flag filters the linters already defined by the configuration, retaining only those classified as fast. It does not replace the configured policy with an unrelated set.

Do not rely solely on the presence of .golangci.yml as proof that it was loaded. As a temporary verification exercise, introduce an issue targeted by one of the explicitly enabled linters—for example, an unclosed HTTP response body for bodyclose—and run golangci-lint run from the module root. Confirm that the expected finding appears, then remove the deliberate issue. This also catches mistakes such as editing a different configuration file from the one used by the command.

Use the same policy in CI

The pipeline should install the project’s selected release and invoke the same command developers use locally:

golangci-lint run

Pinning the runner limits changes caused by newly added linters or upstream linter upgrades. Committing the v2 YAML keeps the enabled set and settings with the source. Together, those choices remove two common sources of “works locally” disagreements: different runner releases and different linter policies.

If fast-only checks are useful during development but the full configured set is required in CI, make that distinction explicit. Otherwise, use the identical command in both places. Reproducibility is mostly the unglamorous art of making sure everyone is running the same thing.

Comments

No comments yet.

Leave a comment

Your comment appears after approval. Plain text only; links stay as text. Line breaks and indentation are kept.

Up to 5,000 characters. No account or email needed.

All articles

Type at least two characters.

Press <kbd>Esc</kbd> to closeOpen the search page