← Back to list

6 Go Linters That Earn a Blocking Spot in CI (and 3 Everyone Enables That Don’t)

The three-question test that decides whether a linter guards your merge button or trains your team to ignore it.

Daniel Valev in Towards Dev · 2026-07-08 13:01 · 0 claps · 5.2 min read paywalled
#golang #devops #programming #continuous-integration #static-analysis
Open on Medium ↗
Wiki topics: 💻 · Programming ☁️ · DevOps & Cloud

6 Go Linters That Earn a Blocking Spot in CI (and 3 Everyone Enables That Don’t)

The three-question test that decides whether a linter guards your merge button or trains your team to ignore it.

Run golangci-lint help linters today and you get over a hundred linters. Enabled by default: five. Every Go team lives somewhere between those two numbers, and where you land decides something expensive: whether a red CI check means "stop, this is a bug" or "click re-run and move on."

I’ll put my wrong assumption in writing. For years, I believed a stricter lint config was a safer one, and that a 40-linter YAML copied from a blog post was rigor. Reading through the golangci-lint v2 docs, changelog, and a depressing number of “how do I silence this” GitHub issues finally killed that belief. Strictness isn’t the metric. Trust is.

Every ignored warning trains the team to ignore the next one.

The criteria (this is the whole article, really)

To earn a blocking spot in CI here, a linter had to pass three tests, checked against the official docs and its issue tracker:

  1. A finding is almost always a real bug, not a style opinion.
  2. The fix is mechanical. No design debate required in the PR.
  3. False positives are rare enough that //nolint stays an event, not a habit.

A linter that fails these isn’t harmless. It’s a smoke detector that goes off every time you cook: you don’t get a safer kitchen, you get a detector with the battery pulled out. All configs below use the v2 format (docs), current as of v2.12.2.

1. errcheck: but only tuned

What it is: the default linter that flags return errors you silently dropped.

It earns its place the day a defer f.Close() or an ignored type assertion turns into a production incident nobody can trace. But the default settings leave its best check off. Type assertions like v := x.(T) panic at runtime, and errcheck stays quiet about them unless you ask:

version: "2"
linters:
  settings:
    errcheck:
      check-type-assertions: true

The maintainers keep sanding off legitimate noise upstream: since errcheck 1.10, crypto/rand.Read is excluded by default because it's documented never to fail.

Skip the tuning (not the linter) if you own a CLI that writes to stdout constantly. Exclude the fmt.Fprint* family in config instead of littering _, _ = everywhere.

2. staticcheck: now three linters in one

What it is: the deepest bug-pattern analyzer in the ecosystem. In v2, the old gosimple and stylecheck linters were merged into it, so if your config still enables those separately, you're on a stale template.

It earns its place on the SA-class checks: comparing a value that’s always nil, misusing time.Tick in a short-lived function, passing a mutex by value. These are bugs a tired reviewer approves at 17:55 on a Friday.

Sixty-second check that it’s actually running with your config:

linters:
  settings:
    govet:
      enable:
        - shadow

It earns its place on one specific Go trap: an inner err := inside an if block shadows the outer err, the function returns the outer nil, and a real failure evaporates. That bug is invisible in review because both spellings look idiomatic.

Skip blocking mode if you’re bolting it onto a large legacy codebase; the first run will be a wall. Gate the new code only with --new-from-rev=origin/main and burn down the backlog separately.

4. errorlint: because == stopped being safe in 2019

What it is: a linter that finds code that breaks the error-wrapping scheme introduced in Go 1.13: comparing errors with == instead of errors.Is, type-asserting instead of errors.As. Golangci-lint

It earns its place the day a dependency starts wrapping its errors in a minor release. Your if err == sql.ErrNoRows compiled fine, passed review fine, and silently stopped matching. That failure mode is why this check belongs to a machine, not a reviewer.

linters:
  enable:
    - errorlint

Skip it if… honestly, there’s no good reason to skip the Is/As checks. The %w versus %v verb check is the arguable third of this linter; disable errorf in its settings if log-only paths make it noisy, and keep the rest blocking.

5. bodyclose: the connection pool’s bodyguard

What it is: a check that the HTTP response body is closed successfully. Golangci-lint

An unclosed resp.Body doesn't crash anything. It quietly pins connections until the pool starves under load, which is why it's a classic "only hurts in production" bug and a perfect linter job: near-certain bug, one-line fix.

linters:
  enable:
    - bodyclose

Honest limitation: it has documented false positives around helpers that close the body in another function. Those are the rare, legitimate //nolint:bodyclose // closed in streamTo() moments. If you're writing one per week, your HTTP plumbing is the finding.

6. gosec: blocking, but only a curated slice

What it is: the security linter, and the fastest-moving item here. Through 2026 it has been adding rules in batches, including new G7xx-series checks.

It earns its place on the unambiguous rules: hardcoded credentials, math/rand where a token or key is born, SQL built by string concatenation. Those pass all three criteria.

But gosec as a whole does not. Rules like G104 duplicate errcheck, and file-permission checks flag legitimate choices. Enable it, then block on a curated subset and let the rest warn:

linters:
  enable:
    - gosec
  settings:
    gosec:
      includes:
        - G101 # hardcoded credentials
        - G404 # weak RNG in security context
        - G201 # SQL string formatting

Skip blanket enablement if today is day one. Blocking on every rule immediately is how security linters get deleted by Thursday.

7. The removal: gocyclo, dupl, and lll

Here’s the pick that will get me yelled at: the complexity-and-style trio that appears in nearly every starter config I found deserves zero blocking power, and probably zero presence.

Run them through the criteria. Is a gocyclo hit at complexity 16 a bug? No; some state machines are legitimately branchy. Is the fix mechanical? No; “split this function” is a design decision. Is disagreement rare? It’s the whole conversation. Same failure for dupl’s duplication thresholds and lll’s line lengths. These linters don’t block bugs; they schedule arguments.

Even the tool agrees with me quietly: the official docs’ own example config shows gocyclo, errcheck, dupl, and gosec excluded from test files — the project’s canonical example treats them as negotiable, which blocking checks never are. Golangci-lint

There’s also a freshness angle: linters age out as the language improves. Go 1.22’s loop-variable change made the once-essential exportloopref obsolete, and today's copyloopvar exists to flag the now-useless copies people still write out of habit. Yesterday's mandatory linter is today's noise. Complexity gates were never even mandatory.

The quadrant that replaces your 40-linter config

fix is mechanical      fix is a design debate
              ┌──────────────────────┬──────────────────────┐
 near-certain │   BLOCK THE MERGE    │   warn, don't block  │
 bug          │ errcheck, bodyclose, │  gosec (full set),   │
              │ errorlint, shadow,   │  contextcheck        │
              │ staticcheck SA*      │                      │
              ├──────────────────────┼──────────────────────┤
 opinion      │  auto-fix, no human  │   DELETE FROM CI     │
              │  formatters section  │  gocyclo, dupl, lll  │
              └──────────────────────┴──────────────────────┘

Only the top-left quadrant gets to turn CI red. The bottom-left is the formatters section doing its job silently. The right column is where lint configs go to lose their credibility.

Takeaways

  • Five defaults are the floor, not the ceiling; ~100 available linters are nowhere near the target.
  • A blocking linter must pass all three tests: near-certain bug, mechanical fix, rare false positives.
  • errcheck and govet only earn full marks after tuning (check-type-assertions, shadow).
  • gosec belongs in CI as a curated subset, never as a blanket block on day one.
  • Complexity thresholds are design opinions with exit codes. Move them to code review, where opinions belong.

I write weekly about DevOps, backend engineering, and security, follow so you don’t miss it.


메타데이터
post_id
30fe6b5f77f7
slug
6-go-linters-that-earn-a-blocking-spot-in-ci-and-3-everyone-enables-that-dont-30fe6b5f77f7
url
https://towardsdev.com/6-go-linters-that-earn-a-blocking-spot-in-ci-and-3-everyone-enables-that-dont-30fe6b5f77f7
canonical_url
https://towardsdev.com/6-go-linters-that-earn-a-blocking-spot-in-ci-and-3-everyone-enables-that-dont-30fe6b5f77f7
author_url
https://medium.com/@danielvalev
status
ok
fetched_at
2026-07-13 06:23:13