Upgrading Golangci-lint to v2


Golangci-lint v2 was released in March 2025 , bringing major architectural improvements and a cleaner configuration format. Despite being available for over seven months, adoption remains low.

If you are maintaining a Go project, now is the time to upgrade. This article covers why you should make the switch and provides a step-by-step walkthrough using a real-world project.

Screenshot of Golangci-lint v2 website (dark theme)


ℹ️ Author note

I have contributed to Golangci-lint development since 2020 , reviewed pull requests for v2 changes, and authored the official migration guide.

Why migrate from v1 to v2

v1 is deprecated

The final v1 release is v1.64 (Feb 12, 2025). It supports Go 1.24 but will not support Go 1.25 or newer. Staying on v1 means missing out on bug fixes, new linters, and future language support. The latest version is v2.6 .

Revamped configuration

The v2 configuration is more consistent and logical:

  • Changed linter settings (see this proposal ).
  • Updated exclusion rules (see this proposal ).
  • Changed some default values.
  • Removed obsolete flags.

These changes make v1 and v2 configurations incompatible, but migration is straightforward.

New fmt command

v2 introduces a dedicated fmt command. It acts like gofmt on steroids, running multiple formatters in a single pass: gci, gofmt, gofumpt, goimports, golines, and swaggo.

Better documentation

The v2 website has been redesigned for easier navigation and better search functionality.

v1v2
Screenshot of Golangci-lint v1 legacy website
Screenshot of Golangci-lint v2 website (light theme)

See this PR for details.


Screenshot of Golangci-lint v2 website (dark theme)

Golangci-lint v2 website (dark theme)

Migration caveats

To help users migrate, Golangci-lint includes the migration command and a migration guide .

Migration is generally easy thanks to the migration command, but there are two main caveats.

Comments are not preserved

The migration tool parses the YAML structure but cannot retain comments (see this PR for explanations). You will need to manually copy comments from your old v1 config to the new v2 file after running the command.

v1 .golangci.ymlv2 .golangci.yml
linters:
  # Enable specific linter.
  enable:
    - govet
version: "2"
linters:
  enable:
    - govet

Deprecated options from v1 or unknown fields are removed

Deprecated linters and settings are removed entirely in v2.

v1 .golangci.ymlv2 .golangci.yml
run:
linters:
  enable:
    - cyclop
    - deadcode
linters-settings:
  cyclop:
    skip-tests: true
version: "2"
linters:
  enable:
    - cyclop

Example: migrating the Lima project

To demonstrate a real-world migration, I upgraded Lima , a popular tool (19k+ stars) for launching Linux VMs.

ℹ️ Note

The steps below use Lima as an example. While the process (install, migrate, fix) is universal, the specific linter exclusions shown here are specific to the Lima codebase.

Every migration of Golangci-lint to v2 consists of the following steps:

  1. Install Golangci-lint v2.
  2. Run golangci-lint migrate.
  3. Manually migrate comments from the v1 to v2 configuration file.
  4. Run Golangci-lint and deal with new lint issues.
  5. Upgrade the Golangci-lint version in CI.

The PR with the Golangci-lint migration in Lima, contributed by me, can be found here . Below is a step-by-step guide showing how I did it.

Let’s clone the project and switch to commit 0625d0b0 with the Golangci-lint v1 configuration :

$ git clone https://github.com/lima-vm/lima.git
$ git switch 0625d0b0 -c chore/migrate-golangci-lint-v2
Switched to a new branch 'chore/migrate-golangci-lint-v2'

The Golangci-lint v1 configuration file in Lima has non-default linter configurations, many comments, deprecated linters, and several settings that changed in v2.

View .golangci.yml (v1) before migration

Install Golangci-lint v2

The installation manual on the official site is comprehensive and understandable :

$ brew install golangci-lint
$ golangci-lint version
golangci-lint has version 2.6.2 built with go1.25.4 from dc16cf4 on 2025-11-14T02:47:46Z

Run golangci-lint migrate

The following command automatically detects the .golangci.yml configuration and migrates to v2 in-place:

$ golangci-lint migrate
WARN The configuration comments are not migrated. 
WARN Details about the migration: https://golangci-lint.run/docs/product/migration-guide/ 
WARN The configuration `run.timeout` is ignored. By default, in v2, the timeout is disabled. 
╭───────────────────────────────────────────────────────────────────────────╮
│                                                                           │
│                               We need you!                                │
│                                                                           │
│ Donations help fund the ongoing development and maintenance of this tool. │
│  If golangci-lint has been useful to you, please consider contributing.   │
│                                                                           │
│                  Donate now: https://donate.golangci.org                  │
│                                                                           │
╰───────────────────────────────────────────────────────────────────────────╯

View .golangci.yml (v2) after golangci-lint migrate

Migration changes

The main difference in the v2 configuration from v1 is version: "2" at the beginning:

v1 .golangci.ymlv2 .golangci.yml
# no version information means v1
version: "2"

The run.timeout setting is removed, which means no execution time limit by default:

v1 .golangci.ymlv2 .golangci.yml
run:
  timeout: 2m
run:
  # no timeout

The next change is the setting for disabling all default linters:

v1 .golangci.ymlv2 .golangci.yml
linters:
  disable-all: true
linters:
  default: none

Updated linters in the enable setting. The list is sorted alphabetically. gofmt, gofumpt, and goimports are moved to formatters. typecheck is not a linter and was removed. gosimple and staticcheck are combined into staticcheck.

v1 .golangci.ymlv2 .golangci.yml
linters:
  enable:
    - staticcheck
    - gofmt
    - gofumpt
    - gosimple
    - revive
    - goimports
    - govet
    - typecheck
linters:
  enable:
    - govet
    - revive
    - staticcheck
formatters:
  enable:
    - gofmt
    - gofumpt
    - goimports

linters-settings are moved to linters.settings:

v1 .golangci.ymlv2 .golangci.yml
linters-settings:
  errorlint:
    asserts: false
linters:
  settings:
    errorlint:
      asserts: false

The issues.exclude-rules settings are moved to linters.exclusions.rules. The issues.include settings are moved to linters.exclusions.presets. Note that in v2, exclusions.paths are added that were always excluded by v1.

v1 .golangci.ymlv2 .golangci.yml
issues:
  include:
    - EXC0013
    - EXC0014
  exclude-rules:
    - path: "pkg/osutil/"
        text: "uid"
    - path: _test\.go
        linters:
          - godot
    - text: "exported: comment on exported const"
        linters:
          - revive
    - text: "fmt.Sprint.* can be replaced with faster"
        linters:
          - perfsprint
linters:
  settings:
  exclusions:
    presets:
      - common-false-positives
      - legacy
      - std-error-handling
    rules:
      - path: pkg/osutil/
        text: '(?i)(uid)|(gid)'
      - linters:
          - godot
        path: _test\.go
      - linters:
          - perfsprint
        text: fmt.Sprint.* can be replaced with faster
  paths:
    - third_party$
    - builtin$
    - examples$

Manually migrate comments from v1 to v2 configuration file

The old v1 configuration file is kept in .golangci.bck.yml, so we can compare changes and add comments to the v2 configuration manually:

$ git status -s
 M .golangci.yml
?? .golangci.bck.yml

View .golangci.yml after copying comments from .golangci.bck.yml

Run Golangci-lint and deal with new lint issues

Run golangci-lint run:

$ golangci-lint run > golangci-lint-run-after-migrate.txt
$ tail -6 golangci-lint-run-after-migrate.txt
577 issues:
* noctx: 48
* nolintlint: 1
* perfsprint: 5
* revive: 449
* staticcheck: 74

View the full golangci-lint run log

A lot of issues, and you might feel confused, right? But it’s not so bad. Most of them can be easily excluded and fixed later.

First, enable comments exclusion preset to suppress comment-related issues:

  exclusions:
    generated: lax
    presets:
      - comments # <-- this line added
      - common-false-positives
      - legacy
      - std-error-handling

This reduces the number of issues from 577 to 72:

$ golangci-lint run
...
72 issues:
* noctx: 48
* nolintlint: 1
* perfsprint: 5
* revive: 5
* staticcheck: 13

Next, apply these changes:

  • Exclude QF and ST1001 checks from staticcheck.
  • Exclude new noctx issues for net.Dial, net.Listen, and exec.Command.
  • Disable the concat-loop check for perfsprint.
  • Allow using Uid and Gid in pkg/osutil.
  • Rename loggerWithoutTs to loggerWithoutTS to satisfy staticcheck.
  • Disable staticcheck for isColimaWrapper__useThisFunctionOnlyForPrintingHints__ (generated code).
  • Remove the nolint comment to fix the nolintlint issue.
linters:
  settings:
    perfsprint:
      int-conversion: false
      err-error: false
      errorf: true
      sprintf1: false
      strconcat: false
      concat-loop: false # <-- this disables concat-loop
    staticcheck:
      checks:
        - all
        - "-SA3000"
        - "-ST1001" # <-- this disables warn about using dot imports
        - "-QF*" # <-- this disables QF checks

  exclusions:
    generated: lax
    rules:
      - linters:
          - noctx
        text: "os/exec.Command must not be called."
      - linters:
          - noctx
        text: "net.* must not be called."
    rules:
      # Allow using Uid, Gid in pkg/osutil.
      - path: pkg/osutil/
        text: '(?i)(uid)|(gid)'

These changes eliminate the remaining issues:

$ golangci-lint run
0 issues.

Additionally, you can remove generated and the default exclusion paths because they are not used in Lima:

linters:
  exclusions:
    generated: lax
    paths:
      - third_party$
      - builtin$
      - examples$

View the final migrated Golangci-lint configuration

Now you can remove .golangci.bck.yml, as it’s no longer needed.

Upgrade the Golangci-lint version in CI

Lima uses the following GitHub Actions workflow configuration to run Golangci-lint:

- name: Run golangci-lint
  uses: golangci/golangci-lint-action@55c2c1448f86e01eaae002a5a3a9624417608d84  # v6.5.2
  version: v1.64.2
  args: --verbose --timeout=10m

All you need to do is update the golangci-lint-action and the version to the latest:

- name: Run golangci-lint
  uses: golangci/golangci-lint-action@e7fa5ac41e1cf5b7d48e45e42232ce7ada589601  # v9.1.0
  version: v2.6
  args: --verbose

You can also remove the --timeout flag since this option is managed via the .golangci.yml configuration file.

Conclusion

Migrating to Golangci-lint v2 ensures your project remains compatible with modern Go versions and benefits from a stricter, more reliable configuration system.

See the PR as a migration example: https://github.com/lima-vm/lima/pull/3330 .

Support the Golangci-lint team: https://donate.golangci.org .


See also