golangci-lint
golangci-lint isn't a linter. It's a runner that bundles over a hundred of them — vet and staticcheck included — loads and type-checks your code once, runs them all in parallel against that shared view, and caches the result. One fast command instead of three tools each re-parsing your project, which is why it's what most Go projects run in CI.
brew install golangci-lint # macOS; released binaries are the documented install
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latestOut of the box, v2 enables five: errcheck, govet, ineffassign, staticcheck, and unused. This file compiles, runs, and go vet ./... finds nothing in it:
package main
import (
"fmt"
"strings"
)
func shout(s string) string {
return strings.ToUpper(s)
}
func main() {
name := strings.TrimSpace(" Dr. Haleh ")
greeting := "hello"
greeting = "hello, " + name
isDoctor := strings.HasPrefix(name, "Dr.")
if isDoctor == true {
greeting = "good day, " + name
}
fmt.Println(greeting)
}golangci-lint run ./...main.go:15:2: ineffectual assignment to greeting (ineffassign)
main.go:19:5: S1002: should omit comparison to bool constant, can be simplified to isDoctor (staticcheck)
main.go:8:6: func shout is unused (unused)Three findings from three different linters, each a fair complaint: an assignment overwritten before anything reads it, a comparison to true on a value that's already a bool, and a helper nobody calls. Notice each line names the linter that produced it — that's how you know what to configure or turn off.
Configuration goes in .golangci.yml at the repo root:
version: "2"
linters:
enable:
- errcheck
- govet
- ineffassign
- staticcheck
- unused
- misspellThe version: "2" line is required — v2 changed the config format and a v1-era file won't load. Start near the defaults; enabling all hundred is a reliable way to end up ignoring the tool.
Some findings have a mechanical fix, and --fix applies them:
isDoctor := strings.HasPrefix(name, "Dr.")
if isDoctor == true {
greeting = "good day, " + name
}isDoctor := strings.HasPrefix(name, "Dr.")
if isDoctor {
greeting = "good day, " + name
}It fixed that one and left the other two alone, which is correct — only you know whether an unused helper should be deleted or wired up.
So: run go vet always, since it's free and partly runs inside go test anyway. On a real project run golangci-lint run ./..., which includes both vet and staticcheck, and put that same command in CI — it exits non-zero when it finds anything. To silence a single finding, add //nolint:unused // kept for the CLI rewrite on the line, always with a reason.