Balancing Code Quality and Developer Velocity with GolangCI-Lint

When you bring linting into a Go project, the temptation is to flip every switch on.
The result? A flood of red in your first pull request, frustrated developers, and a quick trip to golangci.yml to start turning things off.

A better approach is to start with a balanced linting configuration, one that enforces correctness, encourages readability, and keeps formatting consistent, but doesn’t strangle the team with premature strictness. Over time, you can tighten the rules as the codebase matures.


Here’s an example .golangci.yml configuration we’ve used that hits that balance.

The Configuration

run:
  timeout: 3m
  tests: true
  go: "1.22"

linters-settings:
  staticcheck:
    checks: ["all"]
  govet:
    enable-all: true
  revive:
    rules:
      - name: exported
        severity: warning
  gocyclo:
    min-complexity: 15
  errcheck:
    exclude-functions:
      - (*testing.T).Error
      - (*testing.T).Fatal
      - (*testing.T).Fatalf
      - (*testing.T).Log
      - (*testing.T).Logf
      - (*testing.T).Errorf

linters:
  enable:
    - gosimple
    - govet
    - staticcheck
    - revive
    - errcheck
    - ineffassign
    - gocyclo
    - gofumpt
  disable:
    - depguard
    - dupl

issues:
  exclude-use-default: false
  max-issues-per-linter: 0
  max-same-issues: 0
  exclude-dirs:
    - vendor
  exclude-rules:
    - path: _test\.go
      linters:
        - gocyclo

What This Does

  • Keeps the run short – Three-minute timeout so linting doesn’t block CI forever.
  • Checks test files – But ignores complexity in them (gocyclo disabled for _test.go).
  • Targets Go 1.22 – Ensures rules match the language version.
  • Runs a curated set of linters – Correctness (govet, staticcheck), simplicity (gosimple), style (revive), error handling (errcheck), formatting (gofumpt), complexity (gocyclo), and unused assignments (ineffassign).
  • Starts lenient – High complexity threshold (15), no duplicate code check yet, and no dependency restrictions until module boundaries are firm.

What Will Get Flagged

Code Correctness

  • Unchecked errors (except for specific testing.T methods)
  • Misuse of printf-style formatting
  • Variable shadowing and dead code
  • Assignments to variables that are never used

Code Simplicity

  • Overly verbose code patterns that can be simplified
  • Redundant type conversions

Readability & Style

  • Exported functions, types, or constants without a doc comment will get a warning (not a fail)

Complexity

  • Any non-test function with a cyclomatic complexity over 15

Formatting

  • Anything that gofumpt would fix, including spacing, import grouping, and blank lines

What Won’t Get Flagged (Yet)

  • Imports from “forbidden” modules (depguard) – turned off until architecture stabilizes
  • Duplicate code blocks (dupl) – disabled to reduce early noise
  • Complexity in _test.go files
  • Ignored errors in common test logging/fatal functions

Why This Approach Works

Early in a project’s life, you want signal without the noise.
This config makes sure:

  • Bugs are caught early – Logic mistakes, unused values, and unchecked errors won’t slip through.
  • Style is enforced consistently – Formatting is handled automatically; debates over whitespace disappear.
  • Developers keep moving – You’re not blocking merges for harmless duplication or rigid dependency rules before they matter.

As the codebase matures, you can:

  • Lower the complexity threshold from 15 to something tighter like 10
  • Enable depguard to lock down imports
  • Turn on dupl to reduce maintenance risk from copy-paste

Putting It Into Practice

Drop this config in your repo’s root as .golangci.yml.
Then, in CI:

golangci-lint run

And for local checks:

brew install golangci-lint
golangci-lint run

Better yet—hook it into pre-commit so developers get instant feedback before pushing code.

Bottom line: This setup gives you strong guardrails without early friction. It’s the difference between guiding a project toward maintainability and dropping a wall of lint errors on day one.


AI Coding Rule: GolangCI-Lint Compliance

Use the following rule in your AI coding tool to make sure that golangci is used when ever there is a change.

**Rule Name:** `golangci-lint-compliance`  
**Purpose:** Ensure all Go code complies with the `.golangci.yml` configuration before commit or merge.

---

## Rule Statement
When writing or modifying Go code, all changes **must** pass:

```bash
golangci-lint run
```

using the repository’s `.golangci.yml` configuration **without introducing new errors or warnings** (except those explicitly allowed in the config).

---

## AI Behavior
When reviewing or generating Go code, the AI **must**:

1. **Check** for violations against `.golangci.yml`.
2. **Suggest fixes** for any detected violations.
3. **Explain** why the change is required, referencing the relevant linter rule.
4. **Auto-format** code with `gofumpt` before finalizing.
5. **Warn** the developer if:
   - Cyclomatic complexity > **15** in non-test code.
   - An exported function/type/constant lacks a doc comment.
   - An error is ignored without being in the allowed test function list.
   - An unused assignment is found.
   - Code can be simplified.
6. **Ignore** violations in:
   - `_test.go` files for complexity checks.
   - Allowed `testing.T` methods in `errcheck`.
   - Dependency or duplication rules (`depguard`, `dupl`) as they are disabled.

---

## Examples

**✅ Acceptable**
```go
// Add sums two integers and returns the result.
func Add(a, b int) int {
    return a + b
}

if err := process(); err != nil {
    return err
}
```

**🚫 Not Acceptable**
```go
func Add(a, b int) int { // Missing doc comment for exported function
    return a + b
}

f, _ := os.Open("file.txt") // Unchecked error, not in exception list
```

Developer Workflow

  1. If violations are found:
    Fix them before committing.
  2. In CI:
    The same command runs—commits that fail linting will be rejected.

Before commit:

golangci-lint run


Conclusion

A well-tuned .golangci.yml is more than just a linting setup, it’s a shared agreement on code quality.
By starting with a balanced set of rules, you give your team clear expectations without overwhelming them. The focus stays on catching real problems, bugs, formatting issues, and obvious complexity, while leaving room for creativity and iteration in the early stages of a project.

Over time, you can tighten the rules as the codebase stabilizes, transforming this configuration from a gentle guide into a stronger safeguard.
Paired with the AI coding rule, your team gets real-time feedback as they code, ensuring fewer CI failures and a smoother development workflow.

In short, this approach turns linting from a developer frustration into a developer ally.