Quick answer
You declared a local variable and never read it. In Go that's a compile error, not a warning — by design. Pick the fix that matches your intent:
- You meant to use it — use it (this often reveals the real bug).
- It's leftover from an edit — delete the declaration.
- You need the call but not the value — assign to the blank identifier
_. - Appeared after an
if err != nil? It's almost certainly:=shadowing.
The exact error string
package main
import "fmt"
func main() {
count := 42
fmt.Println("hello")
}
// ./main.go:6:2: declared and not used: count
//
// older Go toolchains phrased it as:
// ./main.go:6:2: count declared but not used
Both wordings mean the same thing, and the message names the variable and the exact line where it was declared. Note the emphasis: not used means never read. Writing to a variable does not count — more on that below, because it surprises people coming from almost any other language.
Why Go makes this an error
Most languages emit a warning here, or say nothing at all. Go's authors deliberately made it fatal, for the same reason an unused import is also a hard error: an unused variable is nearly always one of two things, and both are worth stopping for.
The first is dead code — a leftover from an edit, a variable whose consumer you deleted, a half-finished refactor. Left alone it accumulates: every reader afterwards has to work out whether it matters. The second, and more valuable to catch, is a genuine bug: you computed something and then used the wrong name, so the value you carefully derived is silently thrown away while some other variable is read instead.
Warnings don't solve this, because in practice developers learn to ignore warnings once there are more than a handful. Making it an error means the condition can never accumulate — which is why you essentially never encounter unused-variable rot in real Go codebases. The cost is that it interrupts you mid-experiment; the payoff is that the interruption sometimes reveals a real defect.
Fix 1: actually use it
Before reaching for a way to silence the compiler, check whether the error is telling you something. If you declared the variable, you presumably intended to use it — so where did the use go?
// ❌ total is computed, then ignored — the real bug is on the Println line
func main() {
total := price * quantity
fmt.Println("Order total:", price) // ← reading `price`, not `total`
}
// ✅
func main() {
total := price * quantity
fmt.Println("Order total:", total)
}
This is the case that justifies the whole rule. In a language that merely warned, that program would compile and print a wrong number to a customer.
Fix 2: delete it
If the variable really is leftover, remove the declaration. Your editor almost certainly does this for you — gopls (the Go language server) offers a quick-fix, and gofmt's companion goimports handles the equivalent cleanup for imports automatically on save.
func main() {
count := 42 // ← just delete this line
fmt.Println("hello")
}
Fix 3: the blank identifier _
Sometimes you must call a function — for its side effect, or because it returns multiple values — but genuinely don't need one of the results. That's what Go's blank identifier is for. _ is a write-only placeholder: anything assigned to it is discarded, and because it isn't a variable, there's nothing for the compiler to consider unused.
// ignore the error from a conversion you know is safe
value, _ := strconv.Atoi("42")
// ignore the index in a range loop
for _, item := range items {
fmt.Println(item)
}
// ignore the value, keep the index
for i, _ := range items { ... }
for i := range items { ... } // ✅ idiomatic — the _ is unnecessary here
Use it honestly, though. _ = someVariable sprinkled through real code to quiet the compiler is worse than the original problem: it keeps the dead code and hides the signal. Reserve _ for values you're deliberately discarding, not as a way to postpone a decision. (Ignoring an error with _ deserves particular care — that's how a failure becomes invisible and later surfaces as a nil pointer dereference further down the call stack.)
The most common trigger: := shadowing
If this error appeared right after you added error handling, shadowing is almost certainly why. The short variable declaration := creates a new variable whenever the name doesn't already exist in the current block — and an if or for body is its own block, so := there shadows an outer variable of the same name instead of assigning to it:
package main
func load(useCache bool) error {
var err error
if useCache {
_, err := readCache() // := creates a NEW inner err — shadows the outer one
if err != nil {
return err
}
}
return nil
}
// ./main.go:4:6: declared and not used: err
The inner err declared inside the if block is a completely different variable from the outer one — it's read by the if err != nil check right below it, so it compiles fine. But the outer err, declared at the top for the express purpose of holding this result, is never touched again: nothing ever assigns to it after the var line, and nothing ever reads it, because return nil ignores it entirely. That outer variable is what the compiler flags. Use = instead of := so the assignment lands on the existing outer variable rather than creating a new one:
func load(useCache bool) error {
var err error
if useCache {
_, err = readCache() // = assigns to the OUTER err — no shadow, no new variable
if err != nil {
return err
}
}
return err
}
Note the trade-off this reveals: the buggy version above only fails to compile because the outer err happened to end up completely unused. Change the last line from return nil to return err and the outer variable becomes "used" again (its permanent zero value gets read) — the code compiles, but now silently swallows every cache-read failure. Shadowing bugs are dangerous precisely because whether they surface as a compile error or a silent runtime bug depends on details like this, not on whether the underlying mistake is present.
This bug class is common enough that there's a dedicated detector for it — but note it is not part of the default toolchain, so you have to install it first. Skipping the install is why go vet -vettool=$(which shadow) so often fails with a confusing message: which shadow finds nothing, expands to an empty string, and go vet is left with a -vettool= flag pointing at nothing:
go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest
go vet -vettool=$(which shadow) ./...
# reports:
# ./main.go:6:10: declaration of "err" shadows declaration at line 4
It's worth wiring into CI, because the shadowed-variable version often compiles fine (when the outer variable happens to be read elsewhere) and then misbehaves at runtime. Be aware the analyzer is deliberately conservative and reports only shadowing it considers likely to be a mistake, so it catches the common cases rather than every possible shadow.
Why package-level variables are exempt
The rule applies only inside functions. This compiles without complaint:
package main
var unusedConfig = "never read anywhere" // ✅ no error
func main() {}
The reason is provability, not inconsistency. A package-level variable might be read by another file in the same package, or — if exported — by a completely different package the compiler isn't looking at right now. It can't prove the variable is unused. A function-local variable's entire scope is visible in one place, so the compiler can prove nothing ever reads it. Go only enforces the rule where it can be certain.
The same logic explains why unused function parameters are fine: the signature is part of the function's contract, often required to satisfy an interface, so an unused parameter is legitimate.
Assignment is not use
One more nuance that catches people: the variable must be read. Writing to it — even repeatedly — doesn't satisfy the compiler:
package main
func main() {
x := 1
x = 2 // still only ever written
x = 3
}
// ./main.go:4:2: declared and not used: x
Which is exactly right: computing and storing a value nothing ever consults is dead work regardless of how many times you store it.
Quick reference
| Situation | Error? | Fix |
|---|---|---|
| Local variable never read | Yes | Use it, delete it, or _ |
| Local written but never read | Yes | Same — assignment isn't use |
| Package-level variable unused | No | — (can't be proven unused) |
| Unused function parameter | No | — (part of the signature) |
| Unused import | Yes | See imported and not used |
Value assigned to _ | No | — (blank identifier isn't a variable) |
Debugging checklist
- ✓ Read the message — it names the variable and its declaration line
- ✓ First ask: did I mean to use this and read the wrong name instead?
- ✓ Leftover from an edit → delete the declaration (your editor can do it)
- ✓ Genuinely don't need the value → assign to
_ - ✓ Appeared after adding
if err != nil→ check for:=shadowing; use= - ✓ Remember: writing to a variable is not using it — it must be read
- ✓ Package-level and parameters are exempt — only function locals are checked
- ✓ Wire
go vet(with the shadow analyzer) into CI to catch the shadowing class early
Frequently Asked Questions
Why is an unused variable an error in Go and not a warning?
It's a deliberate language design decision. An unused variable is almost always either dead code left behind by an edit, or a genuine bug where you assigned to the wrong name. Go's authors chose to make it impossible to ignore rather than emit a warning developers would learn to skip. The same reasoning makes an unused import an error too. The upshot is that Go code in the wild has essentially no unused-variable rot.
How do I fix 'declared and not used'?
Pick whichever matches your intent: actually use the variable, delete the declaration if it's leftover, or assign to the blank identifier _ if you must call the function for its side effect but genuinely don't need the value. Don't create a fake use like _ = x scattered through real code — that silences the compiler while leaving the dead code in place.
What is the blank identifier _ in Go?
_ is a write-only placeholder that discards whatever is assigned to it. Assigning to _ satisfies the compiler's use requirement without creating a variable, so it's the idiomatic way to ignore a return value you don't need — for example value, _ := strconv.Atoi(s) when you don't care about the error, or for _, v := range items when you don't need the index.
Why don't unused package-level variables cause this error?
The rule applies only to variables declared inside a function. A package-level variable can legitimately be used by another file in the same package, or exported and used by an entirely different package, so the compiler can't conclude it's unused from one file. Function-local variables have no such ambiguity — their scope is fully visible — so the compiler can prove they're never read.
Why do I get it after adding an if err != nil check?
Usually variable shadowing. Inside a block, using := creates new variables rather than assigning to the outer ones, so an inner err (or an inner result) is a different variable from the one outside. If the outer variable is then never read, it's declared and not used. Either use = instead of := to assign to the existing variables, or restructure so there's only one declaration.
Does an assigned-but-never-read variable count as used?
No. Go requires the variable to be read, not merely written. Declaring x, then assigning x = 5 and never reading x, still fails to compile — the assignment doesn't count as a use. That is precisely the point: a value you compute and store but never consult is dead work, and Go makes you either use it or remove it.
More Go & backend errors
Browse the full reference for Go, Rust, and Java errors — exact message, cause, and fix.