Skip to content

How to Configure Go Linters for Improved Code Quality and Efficiency

Posted on:March 9, 2024 at 06:17 PM

How to Configure Go Linters for Improved Code Quality and Efficiency

golangci-lint

Introduction to Go Linters

Go linters are tools that analyze source code to flag programming errors, bugs, stylistic errors, and suspicious constructs. Think of them as automated code reviewers who help maintain high code quality and consistency, making it easier to spot issues early in the development process. This section will introduce the concept of Go linters, covering their importance in the Go programming environment and how they contribute to cleaner, more efficient code.

Setting Up Your Linter

For setting up a Go linter, you typically start by installing the linter using the command line. For instance, if you’re using golangci-lint, which is a popular Go linter, you would install it via Homebrew with the command: brew install golangci-lint or check the documentation golangci-lint. After installation, you can configure it by creating a .golangci.yml file in your project root to define rules and settings.

Configuration Linter Rules

The configuration for golangci-lintbreaks down into four key areas:

  1. Run: This part covers the overall operation settings of the linter, like how many threads to use, timeout settings, and what directories or files to skip. It sets the foundation for how the linter executes its tasks.

  2. Output: This section specifies how the linter’s findings are displayed, including format options (like JSON or plain text) and whether to include certain types of information, ensuring the results are clear and actionable.

  3. Issues & Severity: In this part, you decide how to categorize and act on the issues the linter identifies, including setting severity levels for different types of problems, which helps prioritize fixes.

  4. Linters & Linters-Settings: This crucial section allows you to select which specific linters to activate and how they should operate. Since golangci-lint is actually a suite of linters, this gives you control over which aspects of your code are scrutinized and how rigorously.

Run

The run object is responsible for the golangci-lint run. Here is what I found useful:

run:
  # Depends on your hardware, my laptop can survive 8 threads.
  concurrency: 8
  # I really care about the result, so I'm fine to wait for it.
  timeout: 30m
  # Fail if the error was met.
  issues-exit-code: 1
  # This is very important, bugs in tests are not acceptable either.
  tests: true
  # In most cases this can be empty but there is a popular pattern
  # to keep integration tests under this tag. Such tests often require
  # additional setups like Postgres, Redis etc and are run separately.
  # (to be honest I don't find this useful but I have such tags)
  build-tags:
    - integration
  # Up to you, good for a big enough repo with no-Go code.
  skip-dirs:
    # - src/external_libs
  # When enabled linter will skip directories: vendor$, third_party$, testdata$, examples$, Godeps$, builtin$
  # Skipping `examples` sounds scary to me but skipping `testdata` sounds ok.
  skip-dirs-use-default: false
  # Autogenerated files can be skipped (I'm looking at you gRPC).
  # AFAIK autogen files are skipped but skipping the whole directory should be somewhat faster.
  #skip-files:
  #  - "protobuf/.*.go"
  # With the read-only mode linter will fail if go.mod file is outdated.
  modules-download-mode: readonly
  # Till today I didn't know this param exists, never ran 2 golangci-lint at once.
  allow-parallel-runners: false
  # Keep this empty to use the Go version from the go.mod file.
  go: ""

Discussing the output part, it essentially focuses on the format and presentation of the linting results. These findings are conveniently stored in a file named lint.txt, which you can easily open and review in your code editor to see what needs to be addressed.

output:
  # I prefer the simplest one: `line-number` and saving to `lint.txt`
  #
  # The `tab` also looks good and with the next release I will switch to it
  # (ref: https://github.com/golangci/golangci-lint/issues/3728)
  #
  # There are more formats which can be used on CI or by your IDE.
  format: line-number:lint.txt
  # I do not find this useful, parameter above already enables filepath
  # with a line and column. For me, it's easier to follow the path and
  # see the line in an IDE where I see more code and understand it better.
  print-issued-lines: false
  # Must have. Easier to understand the output.
  print-linter-name: true
  # No, no skips, everything should be reported.
  uniq-by-line: false
  # To be honest no idea when this can be needed, maybe a multi-module setup?
  path-prefix: ""
  # Slightly easier to follow the results + getting deterministic output.
  sort-results: true

issues & severity

Configuring the issues:

issues:
  # I found it strange to skip the errors, setting 0 to have all the results.
  max-issues-per-linter: 0
  # Same here, nothing should be skipped to not miss errors.
  max-same-issues: 0
  # When set to `true` linter will analyze only new code which are
  # not committed or after some specific revision. This is a cool
  # feature when you're going to introduce linter into a big project.
  # But I prefer going gradually package by package.
  # So, it's set to `false` to scan all code.
  new: false
  # 2 other params regarding git integration
  # Even with a recent GPT-4 release I still believe that
  # I know better how to do my job and fix the suggestions.
  fix: false

Run the linter

The main point of the article is about choosing which linters to turn on or off, and I’d like to share a couple of important notes:

  1. The setup I suggest comes from my own experience and what works for me.

  2. Just because I don’t use certain linters doesn’t mean they’re bad or their creators made mistakes. All linters have their value; it depends on the specific needs of your project and team.

Finding the best setup for you: Start by activating all the linters and test them on your project. Look at the feedback they give and decide which ones help you the most. If you’re working in a team, make sure to have a discussion before making a final choice to ensure everyone agrees.

(To see all the linters you can use, type golangci-lint help linters in your terminal).

Remember, what works well for one project may not for another, so be open to adjusting. Let’s dive in!

linters:
  # Set to true runs only fast linters.
  # Good option for 'lint on save', pre-commit hook or CI.
  fast: false

  enable:
    # Check for pass []any as any in variadic func(...any).
    # Rare case but saved me from debugging a few times.
    - asasalint

    # I prefer plane ASCII identifiers.
    # Symbol `∆` instead of `delta` looks cool but no thanks.
    - asciicheck

    # Checks for dangerous unicode character sequences.
    # Super rare but why not to be a bit paranoid?
    - bidichk

    # Checks whether HTTP response body is closed successfully.
    - bodyclose

    # Check whether the function uses a non-inherited context.
    - contextcheck

    # Check for two durations multiplied together.
    - durationcheck

    # Forces to not skip error check.
    - errcheck

    # Checks `Err-` prefix for var and `-Error` suffix for error type.
    - errname

    # Suggests to use `%w` for error-wrapping.
    - errorlint

    # Checks for pointers to enclosing loop variables.
    - exportloopref

    # As you already know I'm a co-author. It would be strange to not use
    # one of my warmly loved projects.
    - gocritic

    # Forces to put `.` at the end of the comment. Code is poetry.
    - godot

    # Might not be that important but I prefer to keep all of them.
    # `gofumpt` is amazing, kudos to Daniel Marti https://github.com/mvdan/gofumpt
    - gofmt
    - gofumpt
    - goimports

    # Allow or ban replace directives in go.mod
    # or force explanation for retract directives.
    - gomoddirectives

    # Powerful security-oriented linter. But requires some time to
    # configure it properly, see https://github.com/securego/gosec#available-rules
    - gosec

    # Linter that specializes in simplifying code.
    - gosimple

    # Official Go tool. Must have.
    - govet

    # Detects when assignments to existing variables are not used
    # Last week I caught a bug with it.
    - ineffassign

    # Even with deprecation notice I find it useful.
    # There are situations when instead of io.ReaderCloser
    # I can use io.Reader. A small but good improvement.
    - interfacer

    # Fix all the misspells, amazing thing.
    - misspell

    # Finds naked/bare returns and requires change them.
    - nakedret

    # Both require a bit more explicit returns.
    - nilerr
    - nilnil

    # Finds sending HTTP request without context.Context.
    - noctx

    # Forces comment why another check is disabled.
    # Better not to have //nolint: at all ;)
    - nolintlint

    # Finds slices that could potentially be pre-allocated.
    # Small performance win + cleaner code.
    - prealloc

    # Finds shadowing of Go's predeclared identifiers.
    # I hear a lot of complaints from junior developers.
    # But after some time they find it very useful.
    - predeclared

    # Lint your Prometheus metrics name.
    - promlinter

    # Checks that package variables are not reassigned.
    # Super rare case but can catch bad things (like `io.EOF = nil`)
    - reassign

    # Drop-in replacement of `golint`.
    - revive

    # Somewhat similar to `bodyclose` but for `database/sql` package.
    - rowserrcheck
    - sqlclosecheck

    # I have found that it's not the same as staticcheck binary :\
    - staticcheck

    # Is a replacement for `golint`, similar to `revive`.
    - stylecheck

    # Check struct tags.
    - tagliatelle

    # Test-related checks. All of them are good.
    - tenv
    - testableexamples
    - thelper
    - tparallel

    # Remove unnecessary type conversions, make code cleaner
    - unconvert

    # Might be noisy but better to know what is unused
    - unparam

    # Must have. Finds unused declarations.
    - unused

    # Detect the possibility to use variables/constants from stdlib.
    - usestdlibvars

    # Finds wasted assignment statements.
    - wastedassign

some linters that I prefer to keep disabled. Again, my use case, yours might be different:

disable:
  # Detects struct contained context.Context field. Not a problem.
  - containedctx

  # Checks function and package cyclomatic complexity.
  # I can have a long but trivial switch-case.
  #
  # Cyclomatic complexity is a measurement, not a goal.
  # (c) Bryan C. Mills / https://github.com/bcmills
  - cyclop

  # Abandoned, replaced by `unused`.
  - deadcode

  # Check declaration order of types, consts, vars and funcs.
  # I like it but I don't use it.
  - decorder

  # Checks if package imports are in a list of acceptable packages.
  # I'm very picky about what I import, so no automation.
  - depguard

  # Checks assignments with too many blank identifiers. Very rare.
  - dogsled

  # Tool for code clone detection.
  - dupl

  # Find duplicate words, rare.
  - dupword

  # I'm fine to check the error from json.Marshal ¯\_(ツ)_/¯
  - errchkjson

  # All SQL queries MUST BE covered with tests.
  - execinquery

  # Forces to handle more cases. Cool but noisy.
  - exhaustive
  - exhaustivestruct # Deprecated, replaced by check below.
  - exhaustruct

  # Forbids some identifiers. I don't have a case for it.
  - forbidigo

  # Finds forced type assertions, very good for juniors.
  - forcetypeassert

  # I might have long but a simple function.
  - funlen

  # Imports order. I do this manually ¯\_(ツ)_/¯
  - gci

  # I'm not a fan of ginkgo and gomega packages.
  - ginkgolinter

  # Checks that compiler directive comments (//go:) are valid. Rare.
  - gocheckcompilerdirectives

  # Globals and init() are ok.
  - gochecknoglobals
  - gochecknoinits

  # Same as `cyclop` linter (see above)
  - gocognit
  - goconst
  - gocyclo

  # TODO and friends are ok.
  - godox

  # Check the error handling expressions. Too noisy.
  - goerr113

  # I don't use file headers.
  - goheader

  # 1st Go linter, deprecated :( use `revive`.
  - golint

  # Reports magic consts. Might be noisy but still good.
  - gomnd

  # Allowed/blocked packages to import. I prefer to do it manually.
  - gomodguard

  # Printf-like functions must have -f.
  - goprintffuncname

  # Groupt declarations, I prefer manually.
  - grouper

  # Deprecated.
  - ifshort

  # Checks imports aliases, rare.
  - importas

  # Forces tiny interfaces, very subjective.
  - interfacebloat

  # Accept interfaces, return types. Not always.
  - ireturn

  # I don't set line length. 120 is fine by the way ;)
  - lll

  # Some log checkers, might be useful.
  - loggercheck

  # Maintainability index of each function, subjective.
  - maintidx

  # Slice declarations with non-zero initial length. Not my case.
  - makezero

  # Deprecated. Use govet `fieldalignment`.
  - maligned

  # Enforce tags in un/marshaled structs. Cool but not my case.
  - musttag

  # Deeply nested if statements, subjective.
  - nestif

  # Forces newlines in some places.
  - nlreturn

  # Reports all named returns, not that bad.
  - nonamedreturns

  # Deprecated. Replaced by `revive`.
  - nosnakecase

  # Finds misuse of Sprintf with host:port in a URL. Cool but rare.
  - nosprintfhostport

  # I don't use t.Parallel() that much.
  - paralleltest

  # Often non-`_test` package is ok.
  - testpackage

  # Compiler can do it too :)
  - typecheck

  # I'm fine with long variable names with a small scope.
  - varnamelen

  # gofmt,gofumpt covers that (from what I know).
  - whitespace

  # Don't find it useful to wrap all errors from external packages.
  - wrapcheck

  # Forces you to use empty lines. Great if configured correctly.
  # I mean there is an agreement in a team.
  - wsl

Linters config

Diving into the specifics of configuring every linter might not be the best use of our time. Instead, I suggest you take a look at the documentation on the Linters page. There, you’ll find all the details needed to fine-tune the linters to match your project’s needs perfectly.

linters-settings:
  # I'm biased and I'm enabling more than 100 checks
  # Might be too much for you. See https://go-critic.com/overview.html
  gocritic:
    enabled-tags:
      - diagnostic
      - experimental
      - opinionated
      - performance
      - style
    disabled-checks:
      # These 3 will detect many cases, but they do sense
      # if it's performance oriented code
      - hugeParam
      - rangeExprCopy
      - rangeValCopy

  errcheck:
    # Report `a := b.(MyStruct)` when `a, ok := ...` should be.
    check-type-assertions: true # Default: false

    # Report skipped checks:`num, _ := strconv.Atoi(numStr)`.
    check-blank: true # Default: false

    # Function to skip.
    exclude-functions:
      - io/ioutil.ReadFile
      - io.Copy(*bytes.Buffer)
      - io.Copy(os.Stdout)

  govet:
    disable:
      - fieldalignment # I'm ok to waste some bytes

  nakedret:
    # No naked returns, ever.
    max-func-lines: 1 # Default: 30

  tagliatelle:
    case:
      rules:
        json: snake # why it's not a `snake` by default?!
        yaml: snake # why it's not a `snake` by default?!
        xml: camel
        bson: camel
        avro: snake
        mapstructure: kebab

output in lint.txt

output in terminal

Automating Linting in Your Workflow

Is about seamlessly integrating the linter into your development process, so it runs automatically at key points, ensuring consistent code quality without adding manual steps for developers. Here’s how to incorporate golangci-lint into various stages of your workflow:

Continuous Integration (CI) Pipelines

Set up golangci-lint to run as part of your CI/CD pipeline. This can be achieved by adding a step in your pipeline configuration (e.g., .gitlab-ci.yml or .github/workflows/ci.yml) that runs golangci-lint whenever new commits are pushed or a pull request is made. This ensures that code is checked against your linting rules before it can be merged into the main branch.

Example of a GitHub Actions workflow:

name: Lint Go Code
on: [push, pull_request]
jobs:
  golangci-lint:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v2
      - name: Run golangci-lint
        uses: golangci/golangci-lint-action@v2
        with:
          version: v1.52.1
          args: --config=.golangci.yml

Pre-commit Hooks

Integrate golangci-lint with pre-commit hooks using a tool like pre-commit. This runs the linter on staged files every time a commit is made, catching issues early in the development cycle. You’ll need to add a .pre-commit-config.yaml to your project and list golangci-lint as a hook.

Example .pre-commit-config.yaml:

repos:
  - repo: https://github.com/golangci/golangci-lint
    rev: v1.52.1
    hooks:
      - id: golangci-lint

Editor Integration

For real-time linting feedback, integrate golangci-lint with your code editor or IDE. Many popular editors like Visual Studio Code, GoLand, or Atom have plugins or settings that allow golangci-lint to run on save or while editing. This instant feedback loop helps developers fix issues on the fly, significantly improving productivity and code quality.

Makefile or Scripts

For projects that use a Makefile or scripts for build and deployment tasks, you can add a lint command that developers can run locally. This provides a quick and easy way to check code before pushing to the repository.

Example Makefile entry:

lint: golangci-lint run

Automating linting in these ways ensures that code quality checks are an integrated part of the development process, requiring minimal effort from developers while maintaining high standards.

Conclusion

Integrating golangci-lint into your Go project is a strategic move towards maintaining high code quality, enhancing readability, and ensuring efficiency.

Remember, the goal of linting isn’t to add a hurdle to your development process but to streamline it, making your codebase more maintainable and your applications more reliable.

If you have questions, need further guidance, or want to share your experience, don’t hesitate to reach out. You can email me at bangadam.dev@gmail.com or visit my website at https://bangadam.space for more insights and discussions on Go development and beyond.

Embrace the power of automated linting, and watch your Go projects thrive in quality and efficiency. Happy coding!