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
- If violations are found:
Fix them before committing. - 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.