1. Introduction
If you are just starting out, it seems logical: “the compiler is smart, it will tell me everything.” The problem is that the compiler must be an honest formalist: if the code is correct by the language rules, it lets it through. But correct code can be weird, fragile, or just suspicious. go vet adds an official layer of “looks like a mistake,” and linters extend this idea: they find more patterns that statistically often turn out to be bugs or sources of pain in code review.
Imagine the compiler as a security guard at a building entrance: “Do you have a pass? Go in.” go vet is the attentive guard who also asks: “Are you sure you’re going there?” And linters are the colleague who has worked here a long time and says: “Listen, you can go in, but people usually fall into a manhole right there—be careful.” Sometimes that colleague is wrong, but more often they save you hours.
To avoid confusion, it helps to keep this split of responsibilities in mind:
| Tool | What it does | What it does not do |
|---|---|---|
|
brings code to the canonical format | does not fix logic |
|
formatting + imports “the right way” | does not heal dependency architecture |
|
official static analysis: “looks like a bug” | does not check every possible suspicion and style issue |
| linters | extended analysis: suspicions + simplifications + style | do not guarantee every warning is true |
2. staticcheck: a practical static analyzer
staticcheck is one of the most popular linters in the Go world because it is quite practical: many of its warnings really point to bugs or to code that can be made simpler without losing clarity. It is important to perceive it in a psychologically healthy way: it is not an “examiner” trying to nitpick, but a tool that tries to reduce the number of silly mistakes and make code more readable.
At the same time, staticcheck is not an “official language standard” in the same sense as gofmt or go vet. It can be more suspicious: sometimes a warning can be ignored if you understand why you wrote it that way. But the key word here is understand. The most dangerous newbie scenario is not understanding the warning, but still “silencing” it just to make things green.
A useful mental model: staticcheck often catches three categories of things.
- “Suspicion of a bug.” For example, you computed a value, assigned it to a variable—and then immediately overwrote it without ever using it. This is often a trace of sloppy refactoring.
- “Meaningless or redundant code.” For example, you compare a bool with true, even though you can write it more simply (and it is easier for the brain to read).
- “Simplifications and idioms.” This is not always about “speed”; more often it is about readability: fewer branches, less noise, fewer places where you can accidentally make a mistake.
3. Practice with examples: a mini task manager
So linters do not feel like a magic box, let’s look at some examples. Imagine we are continuing a learning console application: a simple task manager that stores tasks in memory and prints them. We are not doing “perfect architecture” here; it is more important to learn to see why the linter complains and how that improves the code.
Comparing bool to true: correct code, but noisy
When you are just getting used to if, it is easy to write it “head-on”: if x == true. It works, but in Go people usually do not write like that—because it reads heavier than it should.
package todo
type Task struct {
Title string
Done bool
}
func (t Task) IsDone() bool {
if t.Done == true {
return true
}
return false
}
Everything is correct here, but the thought “is the task done?” got buried under extra words. A linter will most likely suggest simplifying it.
A normal, readable variant:
package todo
type Task struct {
Title string
Done bool
}
func (t Task) IsDone() bool {
return t.Done
}
Why does this matter? Because in a week you will open the code and understand the intent faster. And also, the fewer lines there are, the fewer places there are where you can accidentally do something silly.
Useless assignment: a classic after edits
A very common situation: you calculated something, then changed your mind and “temporarily” set another value, and forgot to delete the old one. The code compiles, tests might not exist, but the logic is no longer what it should be.
package todo
func nextID(tasksCount int) int {
id := tasksCount + 1
id = 1 // accidentally left a "temporary" line
return id
}
It looks almost harmless, but in fact tasksCount no longer affects the result. If the function is supposed to return the next ID, that is already a behavioral bug. staticcheck usually really likes such places and highlights them, because they are often “real.”
The fix depends on what you intended. If you really want tasksCount + 1, then you just remove the junk:
package todo
func nextID(tasksCount int) int {
id := tasksCount + 1
return id
}
Error text: an important habit for the ecosystem
By itself, error messages are part of the developer UX. In Go it is conventional for error text to be lowercase and not start with Error:. Many linters will flag the “capitalized” variant, because later such errors chain poorly and look ugly in logs.
A not-so-great variant:
package todo
import "errors"
func ValidateTitle(title string) error {
if title == "" {
return errors.New("Title is empty")
}
return nil
}
Better:
package todo
import "errors"
func ValidateTitle(title string) error {
if title == "" {
return errors.New("title is empty")
}
return nil
}
It is a small thing, but small things like this save nerves in a big project. In a real application you can make the message even more human-friendly, but right now it is important to catch the style.
Redundant wrapper: when code can be made more direct
Beginners often write “step by step,” and that is fine: it is easier to think that way. But sometimes there are too many steps, and the linter gently hints: “seems like it can be simpler.”
package todo
func Add(a, b int) int {
sum := a + b
return sum
}
This is not an error. But in many projects this style will be simplified because it adds noise. A more direct variant:
package todo
func Add(a, b int) int {
return a + b
}
An important caveat: if the sum variable is needed to explain the meaning (for example, in learning code), then sometimes leaving it is fine. The linter does not forbid an “extra” variable. It suggests: “this can be removed.”
4. golangci-lint: running many linters
When you first hear about golangci-lint, it sounds like something from the world of farm machinery: “went out into the field and harvested a crop of warnings.” Essentially, that is what it is: an aggregator that can run many different linters in one pass and collect the results into a single format.
Why is this convenient? Because without an aggregator you get a zoo: one linter runs one way, another runs differently, a third prints messages in its own style. And you also want the whole team to see the same results.
At the same time, golangci-lint is dangerous for beginners specifically because of its convenience: it is very easy to “enable everything” and get hundreds of messages, half of which are about style, a quarter about subjective preferences, and only a small part about real problems. At that moment your brain says “forget it,” and linters get turned off forever. So the goal is to learn to enable them in a measured way so the signals remain useful.
The good news: golangci-lint is built precisely so you can choose a set of linters, configure exceptions, and live calmly rather than in a constant fire drill.
5. How not to drown in warnings
Almost all problems with linters do not start because of the linters themselves, but because of the wrong tactics. A person sees 300 warnings and tries to heroically fix everything at once—and then hates the linter, the code, and humanity.
A working strategy starts with a simple principle: warnings have different “strength.” Some look like bugs and almost always require a fix. Some are useful advice but do not have to be followed at any cost. It is convenient (even if you do not remember specific check codes) to group signals by meaning.
| Signal “strength” | How to treat it | Example by meaning |
|---|---|---|
| High | “stop and check the logic” | pointless assignment, dead code, suspicious check |
| Medium | “most likely worth simplifying” | extra branching, obvious simplifications |
| Low | “style advice” | cosmetics, message formatting, debatable preferences |
Then the common-sense rule kicks in: if it is a high-strength warning, we almost always fix it. If it is medium, we fix it if it actually improves readability and does not break intent. If it is low, we choose: either we accept the rule as a project standard, or we disable it so it does not create noise.
One more important habit: do not “silence” warnings until you understand what they are about. If you cannot explain out loud what the warning meant and why you are ignoring it, then most likely you are ignoring a potential bug.
Minimal golangci-lint setup
Configuring a linter is a subtle thing: you want it to be “perfect,” and you end up with “nobody runs it.” So beginners are better off starting with a short config: enable a few genuinely useful linters and do not run a contest for the strictest style.
An example of a minimal .golangci.yml that usually provides a good balance:
linters:
enable:
- staticcheck
- govet
- errcheck
run:
timeout: 2m
This is not the “only correct” variant. It is an idea: you enable a small set of linters that more often catch real issues. Then you learn from experience: too noisy—turn it down; too few useful findings—add something surgically.
If some linter constantly argues with your style and makes the code harder to read, it is better to disable it than to “defeat” it on every line. The goal is readable and correct code, not “zero warnings at any cost.”
When it is acceptable to ignore a warning
Sometimes a warning needs to be ignored. Not because you are “lazy,” but because in this specific place you deliberately chose this code. For example, you kept a more verbose variant for teaching clarity, or you truly need non-standard behavior.
But ignoring should be intentional and local. In the Go ecosystem, a common approach is: if you disable a warning, do it as narrowly as possible and preferably with a short explanation.
Often it looks roughly like this (the example is not tied to a specific linter; the idea is what matters):
package todo
import "fmt"
func debugTask(title string) {
//nolint:forbidigo // in this learning example we intentionally print debug output
fmt.Println("debug:", title) // debug: buy milk
}
The idea is: you do not turn off the linter forever; you say “here I understand what I’m doing.” If your //nolint spreads across the project like mold, that is a signal that the rules were chosen poorly or that the code should be rewritten more simply.
6. Typical mistakes when working with linters
Mistake No. 1: enable all linters at once and get a “snow avalanche.”
When there are hundreds of warnings, they stop being signals and turn into background noise. A beginner quickly gets used to ignoring everything, and that kills the point of the tool. It is much more effective to start with a small set and expand only when you truly understand why you need a new rule.
Mistake No. 2: fix a warning without understanding its meaning.
This is more dangerous than it seems. You can “adjust” the code so the linter goes quiet, but the logic becomes worse or a hidden bug appears. The right reaction to a warning is to first understand what exactly is suspicious, and only then change the code. If understanding does not come, it is better to stop and work through the example manually.
Mistake No. 3: make code less readable for the sake of a “green report.”
Sometimes a linter suggests a simplification, but in your context it makes the code less clear, especially in learning examples. That is normal. A linter is an advisor, not a law. If a change reduces clarity, it is better to leave it as-is or relax the rule than to turn the code into a “victory over the linter” at the reader’s expense.
Mistake No. 4: treat a linter as a replacement for tests or as the ultimate truth.
Linters are static analysis: they are very strong at finding some classes of problems, but they do not validate program behavior under scenarios. And linters can be wrong or unaware of your business rules. So the right stance is calm: linters complement the compiler and go vet, but they replace neither thinking nor behavior checks.
Mistake No. 5: silence warnings in bulk instead of configuring the rules.
If a project is covered in //nolint, it means you are fighting the tool rather than using it. Usually it is better to do the opposite: configure the list of linters and rules so that the overwhelming majority of warnings are truly meaningful. Then the rare //nolint will look like a deliberate exception rather than a lifestyle.
GO TO FULL VERSION