Modern Go development demands rigorous code quality standards. Linters for Go automate the detection of bugs, security vulnerabilities, and style inconsistencies before they reach production.
The State of Go Linting in 2025
Go's simplicity and strong conventions make it an ideal language for automated code analysis. The ecosystem has matured significantly, with tools that catch everything from subtle logic errors to performance bottlenecks. The question facing Go developers today isn't whether to use linters, but which combination provides the best balance of thoroughness and speed. If you're new to Go or need a quick reference, check out our comprehensive Go Cheatsheet for essential commands and syntax.
What is the best linter for Go in 2025? The answer is overwhelmingly golangci-lint, a meta-linter that aggregates over 50 individual linters into a single, blazingly fast tool. It has become the de facto standard, used by major projects like Kubernetes, Prometheus, and Terraform. Unlike running multiple linters sequentially, golangci-lint executes them in parallel with intelligent caching, typically completing in seconds even on large codebases.
The core advantage of golangci-lint lies in its unified configuration and output. Instead of managing separate tools with different CLI flags and output formats, you define everything in a single .golangci.yml file. This consistency is invaluable for team collaboration and CI/CD integration.
Essential Linters and Their Purpose
golangci-lint: The All-in-One Solution
golangci-lint serves as the foundation of modern Go code quality. Install it with:
# Binary installation (recommended)
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin
# Or via Go install
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
How do I configure golangci-lint for my project? Start with this baseline .golangci.yml:
linters:
enable:
- staticcheck
- gosimple
- govet
- errcheck
- gosec
- revive
- gocyclo
- misspell
- unconvert
- unparam
linters-settings:
errcheck:
check-type-assertions: true
check-blank: true
govet:
enable-all: true
gocyclo:
min-complexity: 15
revive:
severity: warning
run:
timeout: 5m
tests: true
skip-dirs:
- vendor
- third_party
issues:
exclude-use-default: false
max-issues-per-linter: 0
max-same-issues: 0
This configuration enables critical linters while keeping build times reasonable. Adjust gocyclo complexity and revive rules based on your team's standards.
staticcheck: Deep Static Analysis
What is staticcheck and why is it recommended? staticcheck represents the gold standard of Go static analysis. Maintained by Dominik Honnef since 2016, it implements over 150 checks organized into categories:
- SA (Static Analysis): Bugs and correctness issues
- S (Simple): Simplifications and code improvements
- ST (Stylecheck): Style and naming conventions
- QF (Quick Fixes): Issues with automatic fixes available
- U (Unused): Unused code detection
staticcheck excels at finding subtle bugs that escape human review:
// staticcheck catches this common mistake
func processData(ctx context.Context) {
go func() {
// SA1012: context.Context should not be stored in a struct
// or passed around after the function returns
doWork(ctx)
}()
}
// staticcheck detects inefficient string concatenation
func buildString(items []string) string {
s := ""
for _, item := range items {
s += item // SA1024: use strings.Builder
}
return s
}
Run staticcheck standalone for detailed analysis:
staticcheck ./...
staticcheck -f stylish ./... # Prettier output
staticcheck -checks SA1*,ST* ./... # Specific categories
gofmt and goimports: Formatting Standards
Should I use gofmt or goimports? Always use goimports - it's a strict superset of gofmt. While gofmt only formats code, goimports also manages imports automatically:
# Install goimports
go install golang.org/x/tools/cmd/goimports@latest
# Format all Go files
goimports -w .
# Check without modifying
goimports -d .
goimports handles tedious import management:
// Before goimports
import (
"fmt"
"github.com/pkg/errors"
"os"
)
// After goimports (automatically sorted and organized)
import (
"fmt"
"os"
"github.com/pkg/errors"
)
Configure your editor to run goimports on save. For VSCode, add to settings.json:
{
"go.formatTool": "goimports",
"[go]": {
"editor.formatOnSave": true
}
}
For a completely reproducible development environment that includes all your linting tools and configurations, consider using Dev Containers in VS Code to ensure consistency across your team.
Security-Focused Linting
What security linters should I use for Go? Security must be a first-class concern. gosec (formerly gas) scans for common security issues:
go install github.com/securego/gosec/v2/cmd/gosec@latest
gosec ./...
gosec detects vulnerabilities like:
// G201: SQL string concatenation
db.Query("SELECT * FROM users WHERE name = '" + userInput + "'")
// G304: File path provided as taint input
ioutil.ReadFile(userInput)
// G401: Weak cryptographic primitive
h := md5.New()
// G101: Hardcoded credentials
password := "admin123"
Enable gosec in golangci-lint for continuous security scanning:
linters:
enable:
- gosec
linters-settings:
gosec:
excludes:
- G204 # Audit subprocess command
severity: high
Advanced Linters for Specialized Needs
revive: Flexible Style Enforcement
revive is a faster, more configurable alternative to the deprecated golint. It supports 60+ rules with fine-grained control:
linters-settings:
revive:
rules:
- name: var-naming
severity: warning
arguments:
- ["ID", "URL", "HTTP", "API", "JSON", "XML"] # Allowed initialisms
- name: cognitive-complexity
arguments: [15]
- name: cyclomatic
arguments: [10]
- name: line-length-limit
arguments: [120]
- name: function-length
arguments: [50, 0]
errcheck: Never Miss Error Handling
errcheck ensures you never ignore returned errors - a critical safety net in Go:
// errcheck catches this
file.Close() // Error ignored!
// Should be
if err := file.Close(); err != nil {
log.Printf("failed to close file: %v", err)
}
gopls: IDE Integration
gopls, Go's official language server, includes built-in analysis. Configure it in your editor for real-time feedback:
{
"gopls": {
"analyses": {
"unusedparams": true,
"shadow": true,
"nilness": true,
"unusedwrite": true,
"fieldalignment": true
},
"staticcheck": true
}
}
CI/CD Integration Best Practices
How can I integrate Go linters into CI/CD pipelines? Automated linting in CI prevents code quality regressions. Here's a comprehensive approach:
GitHub Actions
Create .github/workflows/lint.yml:
name: Lint
on:
pull_request:
push:
branches: [main]
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
cache: true
- name: golangci-lint
uses: golangci/golangci-lint-action@v4
with:
version: latest
args: --timeout=5m
# Only show new issues on PRs
only-new-issues: true
GitLab CI
Add to .gitlab-ci.yml:
lint:
image: golangci/golangci-lint:latest
stage: test
script:
- golangci-lint run --timeout=5m --out-format colored-line-number
cache:
paths:
- .golangci.cache
only:
- merge_requests
- main
Docker Integration
Use the official Docker image for consistent environments:
docker run --rm -v $(pwd):/app -w /app golangci/golangci-lint:latest golangci-lint run -v
Make Targets for Local Development
Create a Makefile for convenience:
.PHONY: lint
lint:
golangci-lint run --timeout=5m
.PHONY: lint-fix
lint-fix:
golangci-lint run --fix --timeout=5m
.PHONY: format
format:
goimports -w .
gofmt -s -w .
.PHONY: check
check: format lint
go test -race -coverprofile=coverage.out ./...
go vet ./...
Handling and Fixing Linter Warnings
How do I fix common linter errors in Go? Many issues have automatic fixes:
# Auto-fix what's possible
golangci-lint run --fix
# Fix specific linters only
golangci-lint run --fix --disable-all --enable=goimports,gofmt
# Preview changes without applying
golangci-lint run --fix --out-format=json | jq '.Issues[] | select(.Fixed == true)'
For manual fixes, understand the categories:
Style Issues: Usually safe to fix immediately
// ineffassign: ineffectual assignment
x := 5 // Never used
x = 10
// Fix: remove unused variable
Logic Errors: Require careful review
// nilaway: potential nil pointer dereference
var user *User
fmt.Println(user.Name) // Crashes if user is nil
// Fix: add nil check
if user != nil {
fmt.Println(user.Name)
}
Performance Issues: May need profiling
// prealloc: suggest preallocate slice
var results []string
for _, item := range items {
results = append(results, process(item))
}
// Fix: preallocate
results := make([]string, 0, len(items))
Suppressing False Positives
Sometimes linters flag intentional code. Use //nolint directives sparingly:
// Disable specific linter
//nolint:errcheck
file.Close()
// Disable multiple linters with reason
//nolint:gosec,G304 // User-provided path is validated earlier
ioutil.ReadFile(trustedPath)
// Disable for entire file
//nolint:stylecheck
package main
Document suppressions to help future reviewers understand the context.
Performance Optimization
Large codebases need optimization:
run:
# Use more CPU cores
concurrency: 4
# Cache analysis results
build-cache: true
modules-download-mode: readonly
# Skip generated files
skip-files:
- ".*\\.pb\\.go$"
- ".*_generated\\.go$"
Enable caching in CI for 3-5x speedups:
# GitHub Actions
- uses: actions/cache@v3
with:
path: ~/.cache/golangci-lint
key: ${{ runner.os }}-golangci-lint-${{ hashFiles('**/go.sum') }}
Recommended Configurations by Project Type
Microservices / Production Code
When building production microservices, strict linting is essential. If you're working with databases, also check out our guide on Go ORMs for PostgreSQL to ensure your data layer follows best practices. For advanced integration patterns, see our article on implementing an MCP server in Go.
linters:
enable:
- staticcheck
- govet
- errcheck
- gosec
- gosimple
- ineffassign
- revive
- typecheck
- unused
- misspell
- gocyclo
- dupl
- goconst
- gofmt
- goimports
linters-settings:
gocyclo:
min-complexity: 10
errcheck:
check-type-assertions: true
check-blank: true
gosec:
severity: medium
CLI Tools / Libraries
linters:
enable:
- staticcheck
- govet
- errcheck
- unparam
- unconvert
- misspell
- gofmt
- goimports
- nakedret
- gocognit
linters-settings:
nakedret:
max-func-lines: 30
gocognit:
min-complexity: 20
Experimental / Prototypes
linters:
enable:
- govet
- errcheck
- staticcheck
- gofmt
- ineffassign
run:
tests: false # Skip test linting for speed
issues:
exclude-rules:
- path: _test\.go
linters:
- errcheck
Emerging Trends and Tools
nilaway: Nil Safety Analysis
Uber's nilaway brings nil-safety analysis to Go:
go install go.uber.org/nilaway/cmd/nilaway@latest
nilaway ./...
It catches nil pointer dereferences at compile time - a major source of production crashes. For modern Go applications integrating with AI services, proper error handling and nil safety is crucial - see our comparison of Go SDKs for Ollama for practical examples.
golines: Automatic Line Shortening
golines automatically shortens long lines while maintaining readability:
go install github.com/segmentio/golines@latest
golines -w --max-len=120 .
govulncheck: Vulnerability Scanning
Go's official vulnerability checker scans dependencies:
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...
Integrate it into CI to catch vulnerable dependencies before deployment.
Common Pitfalls and Solutions
Over-Configuration
Don't enable every available linter. Start minimal and add linters as needed. Too many linters create noise and slow down development.
Ignoring Test Code
Lint your tests! They're code too:
run:
tests: true # Analyze test files
issues:
exclude-rules:
# But allow some flexibility in tests
- path: _test\.go
linters:
- funlen
- gocyclo
Not Running Locally
CI-only linting creates friction. Developers should run linters locally with:
# Pre-commit hook
cat > .git/hooks/pre-commit << 'EOF'
#!/bin/sh
make lint
EOF
chmod +x .git/hooks/pre-commit
Or use pre-commit for more sophisticated workflows.
Useful Links
- golangci-lint Documentation
- staticcheck Checks Reference
- gosec Security Rules
- Effective Go Style Guide
- Go Code Review Comments
- gopls Settings Reference
- nilaway GitHub Repository
- Go Cheatsheet
- Go SDKs for Ollama - comparison with examples
- Using Dev Containers in VS Code
- Model Context Protocol (MCP), and notes on implementing MCP server in Go
- Go ORMs for PostgreSQL: GORM vs Ent vs Bun vs sqlc
Conclusion
Go linters have evolved from optional helpers to essential development tools. The combination of golangci-lint for comprehensive checking, staticcheck for deep analysis, goimports for formatting, and gosec for security provides a robust foundation for any Go project.
The key is progressive adoption: start with basic linters, gradually enable more checks, and integrate them into your development workflow and CI/CD pipeline. With proper configuration, linting becomes invisible - catching issues before they become problems while letting developers focus on building features.
Modern Go development isn't about avoiding linters - it's about leveraging them to write better code faster.
Top comments (0)