📖 5 min read (~ 1000 words).

Coding Style

Coding style at go-openapi

TL;DR

Let’s be honest: at go-openapi and go-swagger we’ve never been super-strict on code style and linting.

But perhaps now (2025) is the time to adopt a different stance.

Even though our repos have been early adopters of golangci-lint years ago (we used some other metalinter before), our decade-old codebase is only realigned to new rules from time to time.

Now go-openapi and go-swagger together make up a really large codebase, which is taxing to maintain and keep afloat.

Code quality and the harmonization of rules have thus become things that we need now.

Meta-linter

Universally formatted go code promotes ease of writing, reading, and maintenance.

You should run golangci-lint run before committing your changes.

Many editors have plugins that do that automatically.

We use the golangci-lint meta-linter. The configuration lies in .golangci.yml. You may read the linter’s configuration reference for additional reference.

This configuration is essentially the same across all go-openapi projects.

Some projects may require slightly different settings.

Linting rules posture

Thanks to go’s original design, we developers don’t have to waste much time arguing about code figures of style.

However, the number of available linters has been growing to the point that we need to pick a choice.

Our approach: evaluate, don’t consume blindly

As early adopters of golangci-lint (and its predecessors), we’ve watched linting orthodoxy shift back and forth over the years. Patterns that were idiomatic one year get flagged the next; rules that seemed reasonable in isolation produce noise at scale. Conversations with maintainers of other large Go projects confirmed what our own experience taught us: the default linter set is a starting point, not a prescription.

Our stance is deliberate:

  • Start from default: all, then consciously disable what doesn’t earn its keep. This forces us to evaluate every linter and articulate why we reject it — the disabled list is a design rationale, not technical debt.
  • Tune thresholds rather than disable when a linter’s principle is sound but its defaults are too aggressive for a mature codebase.
  • Require justification for every //nolint directive. Each one must carry an inline comment explaining why it’s there. We maintain a full audit in docs/LINTING.md.
  • Prefer disabling a linter over scattering //nolint across the codebase. If a linter produces systematic false positives on patterns we use intentionally, the linter goes — not our code.
  • Keep the configuration consistent across all go-openapi repositories. Per-repo divergence is a maintenance tax we don’t want to pay.

The result is a three-layer defense: the .golangci.yml config as a baseline, //nolint with mandatory justification for the rare exceptions, and docs/LINTING.md as an audit trail. Contributors should read the disabled list as a set of conscious choices, not gaps to fill.

We enable all linters published by golangci-lint by default, then disable a few ones.

Here are the reasons why they are disabled (update: Feb. 2026, golangci-lint v2.8.0).

  disable:
    - depguard              # we don't want to configure rules to constrain import. That's the reviewer's job
    - exhaustruct           # we don't want to configure regexp's to check type name. That's the reviewer's job
    - funlen                # we accept cognitive complexity as a meaningful metric, but function length is relevant
    - godox                 # we don't see any value in forbidding TODO's etc in code
    - nlreturn              # we usually apply this "blank line" rule to make code less compact. We just don't want to enforce it
    - nonamedreturns        # we don't see any valid reason why we couldn't used named returns
    - noinlineerr           # there is no value added forbidding inlined err
    - paralleltest          # we like parallel tests. We just don't want them to be enforced everywhere
    - recvcheck             # we like the idea of having pointer and non-pointer receivers
    - testpackage           # we like test packages. We just don't want them to be enforced everywhere
    - thelper               # too many false positives on test case factories returning func(*testing.T). See note below
    - tparallel             # see paralleltest
    - varnamelen            # sometimes, we like short variables. The linter doesn't catch cases when a short name is good
    - whitespace            # no added value
    - wrapcheck             # although there is some sense with this linter's general idea, it produces too much noise
    - wsl                   # no added value. Noise
    - wsl_v5                # no added value. Noise

As you may see, we agree with the objective of most linters, at least the principle they are supposed to enforce. But all linters do not support fine-grained tuning to tolerate some cases and not some others.

Note on thelper: the only value we needed from this linter was checking for t.Helper() calls inside genuine test helpers. Unfortunately, it produces persistent false positives on test case factories (functions returning func(*testing.T)), which is a pattern we use extensively with our iter.Seq-based table-driven tests. It also enforces naming conventions we don’t subscribe to. The issue has been reported upstream. We prefer disabling it entirely over maintaining //nolint:thelper directives across every test file.

When this is possible, we enable linters with relaxed constraints.

  settings:
    dupl:
      threshold: 200        # in a older code base such as ours, we have to be tolerant with a little redundancy
                            # Hopefully, we'll be able to gradually get rid of those.
    goconst:
      min-len: 2
      min-occurrences: 3
    cyclop:
      max-complexity: 20    # the default is too low for most of our functions. 20 is a nicer trade-off
    gocyclo:
      min-complexity: 20
    exhaustive:             # when using default in switch, this should be good enough
      default-signifies-exhaustive: true
      default-case-required: true
    lll:
      line-length: 180      # we just want to avoid extremely long lines.
                            # It is no big deal if a line or two don't fit on your terminal.

Final note: since we have switched to a forked version of stretchr/testify, we no longer benefit from the great testifylint linter for tests.