The shared codebase problem
Four developers share a repository. One formats code with tabs, another with spaces. One checks every error return, another wraps them in silent ignores. Pull requests turn into arguments about indentation and missing if err != nil blocks. The solution is not a longer checklist or a stricter code review policy. The solution is a machine that enforces the rules before the code ever reaches a human.
What golangci-lint actually does
golangci-lint is not a single analysis tool. It is a runner that orchestrates dozens of independent Go linters and formatters. Think of it as a security checkpoint that runs multiple scanners in parallel. You tell it which scanners to activate, which to ignore, and what thresholds to use. It catches style violations, logical bugs, and performance anti-patterns in one pass.
The tool works by parsing your source files into an abstract syntax tree. That tree represents the structure of your code without the raw text. Many linters need type information to function correctly. They cannot tell if a variable is a string or a byte slice just by looking at the tokens. The runner compiles your packages in memory, resolves all types, and feeds the fully typed AST to each enabled linter. They analyze the code concurrently. The results merge into a single report. The tool exits with status code zero if everything passes. It exits with status code one if any linter finds a violation. The tool does not guess your intent. It follows your rules exactly.
Your first configuration file
Here is the simplest configuration that establishes a baseline. It disables the default linter set, turns on two focused checks, and enforces canonical formatting.
version: "2"
# version 2 uses the new configuration schema
linters:
default: none
# start with a clean slate instead of inheriting defaults
enable:
- govet
# checks for suspicious constructs like unreachable code
- ineffassign
# catches variables that are assigned but never read
formatters:
enable:
- gofmt
# enforces the official Go formatting standard
Start strict. Relax only when the noise drowns the signal.
How the tool reads your rules
When you run the command, the runner looks for a .golangci.yml or .golangci.yaml file in your project root. It reads the schema, validates the keys, and builds an internal execution plan. The first step is always type resolution. The tool invokes the Go compiler backend to build the packages. This step feels slow on the first run because it processes every dependency. The runner stores the compiled type information in a cache directory. Subsequent runs skip unchanged files and reuse the cached types. The cache makes the second run feel instant. Trust the cache.
The runner then spawns a separate process or goroutine for each enabled linter. They do not run sequentially. They share the cached type information and analyze different files or different aspects of the same file at the same time. The aggregation step collects all findings, deduplicates overlapping warnings, and applies any ignore rules you defined. The final output prints file paths, line numbers, and violation descriptions. The exit code reflects the aggregate result. If you run the tool inside a continuous integration pipeline, the pipeline must respect that exit code. A job that continues after a linter failure defeats the purpose.
A realistic team setup
A production team needs deeper checks than the baseline provides. This configuration adds semantic analysis, error handling enforcement, and stricter formatting rules.
version: "2"
linters:
default: none
enable:
- govet
- ineffassign
- staticcheck
# performs deep semantic analysis and catches common bugs
- errcheck
# ensures every error return is handled or explicitly ignored
- revive
# configurable style linter that replaces the deprecated golint
formatters:
enable:
- gofmt
- gofumpt
# applies stricter rules on top of gofmt
settings:
gofumpt:
extra-rules: true
# enforces additional spacing and alignment conventions
run:
timeout: 5m
# prevents the tool from hanging on large monorepos
concurrency: 4
# limits parallel linter processes to match CPU cores
Configuration is a contract. Write it once, enforce it everywhere.
Common traps and compiler friction
The most common mistake is enabling too many linters at once. A fresh repository will drown in warnings. The tool prints linter "xyz" is not found and stops if you reference a misspelled or removed checker. If you enable both gofmt and gofumpt without understanding their overlap, you might see conflicting formatters enabled because they disagree on spacing rules. Set a realistic timeout. Large codebases get killed with analysis failed: context deadline exceeded when the runner exceeds its memory or time budget.
Another trap is ignoring the exit code in continuous integration. A pipeline that runs the linter but continues to the build step defeats the purpose. The tool must fail the job. Error handling deserves special attention. The errcheck linter will flag every ignored return value. The Go community accepts the if err != nil { return err } boilerplate because it makes the unhappy path visible. Do not disable errcheck to avoid writing error checks. Write the checks instead.
Formatting conventions also cause friction. gofmt is mandatory in the Go ecosystem. Do not argue about indentation or brace placement. Let the tool decide. Most editors run it on save. gofumpt adds extra rules on top of the official formatter. It enforces consistent spacing around type assertions and aligns struct fields. The extra-rules: true setting activates the strictest alignment behavior. If your team prefers the official standard, remove gofumpt from the list. If you want stricter alignment, keep it. A linter that runs but never fails is just a suggestion box.
When to reach for golangci-lint
Use golangci-lint when you need a single command to run multiple static analysis tools in parallel. Use a single linter like staticcheck or revive when you only care about deep semantic analysis or strict style rules. Use go vet directly when you want zero configuration and only need the standard library built-in checks. Use manual code review when you need to evaluate architecture decisions, naming clarity, or business logic that tools cannot measure.
Automate the boring checks. Save human attention for design.