skip to content

Imports, Aliases and Cycles

Go refuses to compile an import cycle at all, so package layout is a design constraint rather than a style preference. Interviewers ask how you break a cycle — usually by moving the shared interface to the consumer side or extracting a third package — and what a blank import is really doing.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

4

Why does the Go compiler reject a file that imports a package it never uses?

level: juniorimportance: must knowfreq 60%

answer

  1. error, not a warning
  2. warnings get ignored
  3. every import costs build graph
  4. underscore marks it deliberate

basics

~20 s

Go 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 s

The 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 lines
go
import (
	"fmt"
	"os" // compile error: "os" imported and not used
)

func greet() {
	fmt.Println("hi")
}

go deeper

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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
open as a page

How do you break a Go import cycle that the compiler rejects between two packages?

level: seniorimportance: must knowfreq 66%

basics

~20 s

Go requires an acyclic package graph and fails the build with import cycle not allowed. Break it by declaring the interface you need in the consuming package, by lifting the shared types into a third package both import, or by merging two packages that were split in the wrong place.

open as a page

In a Go import, which identifier does the import path bind, and when do you alias it?

level: middleimportance: should knowfreq 52%

basics

~20 s

An import path only locates the package. The identifier bound in that file is the package's own declared name from its package clause, which usually but need not match the last path element. Alias when two imports would bind the same name.

open as a page

Why does sql.Open fail with an unknown-driver error in a binary that compiled fine?

level: seniorimportance: should knowfreq 46%

basics

~20 s

The driver package was never linked into the binary. Nothing in your code names it, so the only edge pulling it in is a blank import, an underscore before the path, and a refactor can delete that line without producing any compile error.

open as a page