Knaph

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@latest

Out 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
    - misspell

The 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:

Before --fix
isDoctor := strings.HasPrefix(name, "Dr.")
if isDoctor == true {
	greeting = "good day, " + name
}
After --fix
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.

Sign in to track your progress through this course.

Ask an AI

Open a ready-made prompt in ChatGPT or Claude — just press Enter.

SummarizeChatGPTClaude
Ask me questionsChatGPTClaude
Let's learn togetherChatGPTClaude