Why does the Go compiler reject a file that imports a package it never uses?
answer
- error, not a warning
- warnings get ignored
- every import costs build graph
- underscore marks it deliberate
basics
~20 sGo makes an unused import a hard compile error rather than a warning: dead imports slow builds, grow the dependency graph and mislead readers. Delete the line, or make it deliberate with a blank import, an underscore before the path.
solid answer
~40 sThe build stops with `"os" imported and not used`. Go's designers chose an error over a warning on purpose: a warning nobody fixes is noise, and every import is a real edge in the build graph that costs compile time and drags transitive packages into the binary. The same discipline covers unused local variables (`declared and not used`), while unused package-level variables, function parameters and struct fields are all fine. In practice nobody maintains the import block by hand — editor tooling in the `gofmt` family, such as `goimports`, adds and removes lines on save. When you genuinely need a package linked in without ever naming it, you write `import _ "path"`; the underscore binds no identifier, so there is nothing to be unused, and the omission reads as intentional.
code
go · 8 linesimport (
"fmt"
"os" // compile error: "os" imported and not used
)
func greet() {
fmt.Println("hi")
}go deeper
Be ready to say it is a compile error, not a warning, and to name the fix: delete the import, or write it with an underscore if you need the package only for its side effects.
Explain the rationale — warnings get ignored and every import is a real build-graph edge — and know the boundary: unused locals are errors too, unused package-level variables, parameters and fields are not.
Show how the rule shapes workflow: editor tooling maintains the import block, so reviews never argue about stale imports. Point out that the rule proves every named import is used, which is exactly why a blank import needs a comment.
Frame it as the language trading small daily friction for a dependency graph you can trust. Be ready to say where else you would accept compiler-enforced hygiene over lint rules a team can quietly disable.
## The rule An import declaration introduces a name into the **file block** — the scope covering exactly one `.go` file. If your file imports `os` and no expression in that file mentions `os`, the compiler refuses to build it and prints something like: ./main.go:5:2: "os" imported and not used This is an error, not a diagnostic you can downgrade. There is no compiler flag, no pragma and no comment that suppresses it. Every other Go toolchain behaviour follows from that: the language has no "warnings" tier at all for this class of problem. ## Why an error rather than a warning Two reasons, both practical rather than theoretical. First, **warnings get ignored**. Once a build prints twenty of them, nobody reads the twenty-first, and the signal is gone. Go's position is that if something is worth telling the programmer, it is worth stopping for; if it is not worth stopping for, it does not belong in the compiler. Second, **imports are not free**. An import is an edge in the build graph. It pulls a package — and everything that package imports — into compilation and, for anything actually reachable, into the binary. A stale import that survived a refactor keeps a dependency alive that the code no longer needs, which shows up as slower builds, a larger binary, and a dependency list that misrepresents what the package really depends on. Keeping the graph honest at zero cost to the compiler is worth a small daily friction to the programmer. A third effect is social: because the rule is absolute, no code review ever argues about stale imports, and no lint configuration has to be agreed on. ## What the rule does and does not cover The same strictness applies to **unused local variables**: `x declared and not used`. It deliberately does not apply to: - package-level variables that nothing reads, - function parameters and named results, - struct fields, - unused constants and types. The distinction is that an unused import or an unused local is almost always either a leftover or a bug — you meant to use it and did not — whereas package-level declarations are frequently part of an API that other files or other packages consume, so the compiler cannot make the same judgment locally. ## The escape hatches There are exactly two, and only one of them is idiomatic. The idiomatic one is the **blank import**: `import _ "image/png"`. The underscore is the blank identifier, so the import binds no name; there is no name to be unused, and the compiler is satisfied. The package is still compiled, linked and initialised, which is the whole point of the form — it exists for packages whose value is a side effect of being present rather than an API you call. The scratch one, useful while debugging, is to reference the package once from a package-level declaration: `var _ = fmt.Println`. That keeps a named import alive while you comment out the code that used it. It is fine in a working tree and does not belong in a commit. Similarly, `_ = x` silences an unused local. Both are stopgaps, not style. ## Living with the rule The reason this is a footnote in daily Go rather than an irritant is tooling. `goimports` and the language server's organise-imports action rewrite the import block on every save: they delete what you stopped using and add what you just started using, grouping and sorting the block as `gofmt` would. Most Go programmers never type an import line by hand. ## The consequence worth understanding Because the compiler enforces this rule, you get a real guarantee: **every named import in a Go file is actually used by that file**. That guarantee is exactly why the blank import is a special case worth commenting. A blank import is the one import the compiler cannot check for you — nothing references it, so nothing breaks at build time if it is deleted. That is why the convention is to put a short comment on the line saying which side effect it is there for.
- Does Go treat an unused local variable the same way?Yes — the compiler reports `declared and not used` for a local you never read. Unused package-level variables, function parameters and struct fields are all allowed, because the compiler cannot tell locally whether something else consumes them. If you need to keep a local around while debugging, assigning it to the blank identifier (`_ = x`) silences the error.
- Is there any way to keep an import whose package you never reference by name?Write it as a blank import: `import _ "image/png"`. No identifier is bound, so nothing can be unused, and the package is still compiled, linked and initialised. Convention is a short comment on the line saying which side effect you are relying on, because no tool can infer that for you.
saying these in an interview costs you the question
- Says go vet reports unused imports, not the compiler
- Claims it is a warning you can suppress with a build flag
- Thinks unused package-level variables are errors too
- Believes the linker drops it anyway, so it is harmless