golangci-lint v2: What Broke and How to Fix It¶
You updated golangci/golangci-lint-action to v9. CI failed. Welcome to golangci-lint v2.
The new version has stricter errcheck rules by default. Functions that return errors need handling. Yes, even fmt.Fprintln.
Here's how to fix it properly.
The Breaking Change¶
Action v9 bundles golangci-lint v2. It has stricter error checking:
pkg/output/markdown.go:28:14: Error return value of `fmt.Fprintln` is not checked (errcheck)
fmt.Fprintln(w, "| Metric | Value |")
^
This affects most Go projects. Any code that writes to stdout is hit.
Why This Matters
I/O errors can hide disk-full or broken pipe issues. But for stdout in CLI tools, these errors rarely recover anyway.
Solution Patterns¶
Pattern 1: Explicit Ignore¶
The simplest fix. Mark the ignored return value:
Pros: Minimal change, clear intent Cons: Clutters code when used frequently
Pattern 2: Writer Wrapper (Recommended)¶
For files with many write operations, create a wrapper:
// mw wraps io.Writer to simplify error handling for fmt functions.
// Write errors to stdout are typically unrecoverable.
type mw struct {
w io.Writer
}
func (m mw) println(a ...any) {
_, _ = fmt.Fprintln(m.w, a...)
}
func (m mw) printf(format string, a ...any) {
_, _ = fmt.Fprintf(m.w, format, a...)
}
// Usage
func WriteReport(w io.Writer, data *Report) {
m := mw{w}
m.println("# Report")
m.printf("Total: %d\n", data.Total)
}
Pros: Clean code, centralized decision Cons: Requires refactoring
Pattern 3: Error Accumulator¶
When you want to track the first error:
type errWriter struct {
w io.Writer
err error
}
func (e *errWriter) println(a ...any) {
if e.err != nil {
return
}
_, e.err = fmt.Fprintln(e.w, a...)
}
func (e *errWriter) Err() error {
return e.err
}
Pros: Captures errors for logging Cons: More complex than needed for stdout
Configuration-Based Solutions¶
Exclude Specific Functions¶
# .golangci.yml
version: "2"
linters-settings:
errcheck:
exclude-functions:
- fmt.Fprintln
- fmt.Fprintf
- fmt.Fprint
- (io.Writer).Write
Pros: No code changes Cons: May hide legitimate issues
Exclude by Path¶
# .golangci.yml
version: "2"
issues:
exclude-rules:
- path: pkg/output/
linters:
- errcheck
text: "Error return value of `fmt\\.(Fp|P)rint"
Migration Workflow¶
Step 1: Run Locally First¶
# Install latest golangci-lint
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
# Run linter
golangci-lint run ./...
Never Push Without Local Verification
Don't discover linter failures in CI. Run locally first. See Should Work ≠ Does Work.
Step 2: Categorize Failures¶
Group by type:
- errcheck: Ignored return values (most common)
- govet: Suspicious constructs
- staticcheck: Bug patterns
Step 3: Choose a Fix Pattern¶
For errcheck, decide on:
- Wrapper type (recommended for output-heavy code)
- Explicit ignore (for isolated cases)
- Config exclusion (for specific packages)
Apply the same fix across all files.
Step 4: Verify¶
Common errcheck Patterns¶
Standard Library I/O¶
// All need handling:
fmt.Println("...") // Returns (int, error)
fmt.Fprintf(w, "...") // Returns (int, error)
io.WriteString(w, "...") // Returns (int, error)
w.Write([]byte("...")) // Returns (int, error)
File Operations¶
// File.Close returns error
defer f.Close() // Fails errcheck!
// Fix: explicit ignore for read-only operations
defer func() { _ = f.Close() }()
JSON Encoding¶
Rollback Strategy¶
If you need more time, pin the old version:
This gives you time. But v1 will not get updates.
Troubleshooting¶
Different Local vs CI Results¶
Ensure same version:
# Check local version
golangci-lint --version
# Pin in CI
- uses: golangci/golangci-lint-action@v9
with:
version: v2.7.1 # Explicit version
Cached Results¶
Clear cache if seeing stale results:
The Payoff¶
Stricter rules improve code quality. Error handling becomes clear. The wrapper pattern keeps code clean and the linter happy.
Key steps:
- Update action to v9
- Run linter locally to discover issues
- Choose and apply a consistent fix pattern
- Verify all checks pass
- Push with confidence
Related¶
- Should Work ≠ Does Work - Always verify locally before pushing
- Building GitHub Actions in Go - CI setup for Go projects
- Pre-commit Hooks with Binary Releases - Local linting enforcement